first commit
This commit is contained in:
commit
7e8cddf208
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
.vs
|
||||
ImageSharp/bin
|
||||
ImageSharp/obj
|
||||
ImageSharp.Drawing/bin
|
||||
ImageSharp.Drawing/obj
|
||||
PolygonClipper/bin
|
||||
PolygonClipper/obj
|
||||
SixLabors.Fonts/bin
|
||||
SixLabors.Fonts/obj
|
||||
281
DebugGuard.cs
Normal file
281
DebugGuard.cs
Normal file
@ -0,0 +1,281 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors {
|
||||
/// <summary>
|
||||
/// Provides methods to protect against invalid parameters for a DEBUG build.
|
||||
/// </summary>
|
||||
[DebuggerStepThrough]
|
||||
#pragma warning disable RCS1043 // Remove 'partial' modifier from type with a single part.
|
||||
internal static partial class DebugGuard
|
||||
#pragma warning restore RCS1043 // Remove 'partial' modifier from type with a single part.
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures that the value is not null.
|
||||
/// </summary>
|
||||
/// <param name="value">The target object, which cannot be null.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void NotNull<TValue>([NotNull] TValue? value, [CallerArgumentExpression("value")] string? parameterName = null)
|
||||
where TValue : class =>
|
||||
ArgumentNullException.ThrowIfNull(value, parameterName);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the target value is not null, empty, or whitespace.
|
||||
/// </summary>
|
||||
/// <param name="value">The target string, which should be checked against being null or empty.</param>
|
||||
/// <param name="paramName">Name of the parameter.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="value"/> is empty or contains only blanks.</exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void NotNullOrWhiteSpace([NotNull] string? value, [CallerArgumentExpression("value")] string? paramName = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
ThrowArgumentException("Must not be empty or whitespace.", paramName!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the specified value is less than a maximum value.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is greater than the maximum value.
|
||||
/// </exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void MustBeLessThan<TValue>(TValue value, TValue max, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(max) >= 0)
|
||||
{
|
||||
ThrowArgumentOutOfRangeException(parameterName, $"Value {value} must be less than {max}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is less than or equal to a maximum value
|
||||
/// and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is greater than the maximum value.
|
||||
/// </exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void MustBeLessThanOrEqualTo<TValue>(TValue value, TValue max, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(max) > 0)
|
||||
{
|
||||
ThrowArgumentOutOfRangeException(parameterName, $"Value {value} must be less than or equal to {max}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is greater than a minimum value
|
||||
/// and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is less than the minimum value.
|
||||
/// </exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void MustBeGreaterThan<TValue>(TValue value, TValue min, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(min) <= 0)
|
||||
{
|
||||
ThrowArgumentOutOfRangeException(
|
||||
parameterName,
|
||||
$"Value {value} must be greater than {min}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is greater than or equal to a minimum value
|
||||
/// and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is less than the minimum value.
|
||||
/// </exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void MustBeGreaterThanOrEqualTo<TValue>(TValue value, TValue min, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(min) < 0)
|
||||
{
|
||||
ThrowArgumentOutOfRangeException(parameterName, $"Value {value} must be greater than or equal to {min}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is greater than or equal to a minimum value and less than
|
||||
/// or equal to a maximum value and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is less than the minimum value of greater than the maximum value.
|
||||
/// </exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void MustBeBetweenOrEqualTo<TValue>(TValue value, TValue min, TValue max, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0)
|
||||
{
|
||||
ThrowArgumentOutOfRangeException(
|
||||
parameterName,
|
||||
$"Value {value} must be greater than or equal to {min} and less than or equal to {max}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies, that the method parameter with specified target value is true
|
||||
/// and throws an exception if it is found to be so.
|
||||
/// </summary>
|
||||
/// <param name="target">The target value, which cannot be false.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <param name="message">The error message, if any to add to the exception.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="target"/> is false.
|
||||
/// </exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void IsTrue(bool target, string parameterName, string message)
|
||||
{
|
||||
if (!target)
|
||||
{
|
||||
ThrowArgumentException(message, parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies, that the method parameter with specified target value is false
|
||||
/// and throws an exception if it is found to be so.
|
||||
/// </summary>
|
||||
/// <param name="target">The target value, which cannot be true.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <param name="message">The error message, if any to add to the exception.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="target"/> is true.
|
||||
/// </exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void IsFalse(bool target, string parameterName, string message)
|
||||
{
|
||||
if (target)
|
||||
{
|
||||
ThrowArgumentException(message, parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies, that the `source` span has the length of 'minLength', or longer.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type of the spans.</typeparam>
|
||||
/// <param name="source">The source span.</param>
|
||||
/// <param name="minLength">The minimum length.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="source"/> has less than <paramref name="minLength"/> items.
|
||||
/// </exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void MustBeSizedAtLeast<T>(ReadOnlySpan<T> source, int minLength, string parameterName)
|
||||
{
|
||||
if (source.Length < minLength)
|
||||
{
|
||||
ThrowArgumentException($"Span-s must be at least of length {minLength}!", parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies, that the `source` span has the length of 'minLength', or longer.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type of the spans.</typeparam>
|
||||
/// <param name="source">The target span.</param>
|
||||
/// <param name="minLength">The minimum length.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="source"/> has less than <paramref name="minLength"/> items.
|
||||
/// </exception>
|
||||
[Conditional("DEBUG")]
|
||||
public static void MustBeSizedAtLeast<T>(Span<T> source, int minLength, string parameterName)
|
||||
{
|
||||
if (source.Length < minLength)
|
||||
{
|
||||
ThrowArgumentException($"The size must be at least {minLength}.", parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the 'destination' span is not shorter than 'source'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The source element type.</typeparam>
|
||||
/// <typeparam name="TDest">The destination element type.</typeparam>
|
||||
/// <param name="source">The source span.</param>
|
||||
/// <param name="destination">The destination span.</param>
|
||||
/// <param name="destinationParamName">The name of the argument for 'destination'.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void DestinationShouldNotBeTooShort<TSource, TDest>(
|
||||
ReadOnlySpan<TSource> source,
|
||||
Span<TDest> destination,
|
||||
string destinationParamName)
|
||||
{
|
||||
if (destination.Length < source.Length)
|
||||
{
|
||||
ThrowArgumentException($"Destination span is too short!", destinationParamName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the 'destination' span is not shorter than 'source'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The source element type.</typeparam>
|
||||
/// <typeparam name="TDest">The destination element type.</typeparam>
|
||||
/// <param name="source">The source span.</param>
|
||||
/// <param name="destination">The destination span.</param>
|
||||
/// <param name="destinationParamName">The name of the argument for 'destination'.</param>
|
||||
[Conditional("DEBUG")]
|
||||
public static void DestinationShouldNotBeTooShort<TSource, TDest>(
|
||||
Span<TSource> source,
|
||||
Span<TDest> destination,
|
||||
string destinationParamName)
|
||||
{
|
||||
if (destination.Length < source.Length)
|
||||
{
|
||||
ThrowArgumentException($"Destination span is too short!", destinationParamName);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentException(string message, string parameterName) =>
|
||||
throw new ArgumentException(message, parameterName);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentOutOfRangeException(string parameterName, string message) =>
|
||||
throw new ArgumentOutOfRangeException(parameterName, message);
|
||||
}
|
||||
}
|
||||
1272
Guard.Numeric.cs
Normal file
1272
Guard.Numeric.cs
Normal file
File diff suppressed because it is too large
Load Diff
132
Guard.Numeric.tt.bak
Normal file
132
Guard.Numeric.tt.bak
Normal file
@ -0,0 +1,132 @@
|
||||
<#@ template debug="false" hostspecific="false" language="C#" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
<#@ output extension=".cs" #>
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods to protect against invalid parameters.
|
||||
/// </summary>
|
||||
internal static partial class Guard
|
||||
{
|
||||
<#
|
||||
var types = new[] { "byte", "sbyte", "short", "ushort", "char", "int", "uint", "float", "long", "ulong", "double", "decimal" };
|
||||
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
if (i > 0) WriteLine("");
|
||||
|
||||
var T = types[i];
|
||||
#>
|
||||
/// <summary>
|
||||
/// Ensures that the specified value is less than a maximum value.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is greater than the maximum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeLessThan(<#=T#> value, <#=T#> max, string parameterName)
|
||||
{
|
||||
if (value < max)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is less than or equal to a maximum value
|
||||
/// and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is greater than the maximum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeLessThanOrEqualTo(<#=T#> value, <#=T#> max, string parameterName)
|
||||
{
|
||||
if (value <= max)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is greater than a minimum value
|
||||
/// and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is less than the minimum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeGreaterThan(<#=T#> value, <#=T#> min, string parameterName)
|
||||
{
|
||||
if (value > min)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is greater than or equal to a minimum value
|
||||
/// and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is less than the minimum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeGreaterThanOrEqualTo(<#=T#> value, <#=T#> min, string parameterName)
|
||||
{
|
||||
if (value >= min)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is greater than or equal to a minimum value and less than
|
||||
/// or equal to a maximum value and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is less than the minimum value of greater than the maximum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeBetweenOrEqualTo(<#=T#> value, <#=T#> min, <#=T#> max, string parameterName)
|
||||
{
|
||||
if (value >= min && value <= max)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName);
|
||||
}
|
||||
<#
|
||||
}
|
||||
#>
|
||||
}
|
||||
289
Guard.cs
Normal file
289
Guard.cs
Normal file
@ -0,0 +1,289 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors {
|
||||
/// <summary>
|
||||
/// Provides methods to protect against invalid parameters.
|
||||
/// </summary>
|
||||
[DebuggerStepThrough]
|
||||
internal static partial class Guard
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures that the value is not null.
|
||||
/// </summary>
|
||||
/// <param name="value">The target object, which cannot be null.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void NotNull<TValue>([NotNull]TValue? value, [CallerArgumentExpression("value")] string? parameterName = null)
|
||||
where TValue : class =>
|
||||
ArgumentNullException.ThrowIfNull(value, parameterName);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the target value is not null, empty, or whitespace.
|
||||
/// </summary>
|
||||
/// <param name="value">The target string, which should be checked against being null or empty.</param>
|
||||
/// <param name="parameterName">Name of the parameter.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="value"/> is empty or contains only blanks.</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void NotNullOrWhiteSpace([NotNull]string? value, string parameterName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentExceptionForNotNullOrWhitespace(value, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the specified value is less than a maximum value.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is greater than the maximum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeLessThan<TValue>(TValue value, TValue max, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(max) < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThan(value, max, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is less than or equal to a maximum value
|
||||
/// and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is greater than the maximum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeLessThanOrEqualTo<TValue>(TValue value, TValue max, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(max) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeLessThanOrEqualTo(value, max, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is greater than a minimum value
|
||||
/// and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is less than the minimum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeGreaterThan<TValue>(TValue value, TValue min, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(min) > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThan(value, min, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is greater than or equal to a minimum value
|
||||
/// and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is less than the minimum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeGreaterThanOrEqualTo<TValue>(TValue value, TValue min, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(min) >= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeGreaterThanOrEqualTo(value, min, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the specified value is greater than or equal to a minimum value and less than
|
||||
/// or equal to a maximum value and throws an exception if it is not.
|
||||
/// </summary>
|
||||
/// <param name="value">The target value, which should be validated.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <typeparam name="TValue">The type of the value.</typeparam>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="value"/> is less than the minimum value of greater than the maximum value.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeBetweenOrEqualTo<TValue>(TValue value, TValue min, TValue max, string parameterName)
|
||||
where TValue : IComparable<TValue>
|
||||
{
|
||||
if (value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeBetweenOrEqualTo(value, min, max, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies, that the method parameter with specified target value is true
|
||||
/// and throws an exception if it is found to be so.
|
||||
/// </summary>
|
||||
/// <param name="target">The target value, which cannot be false.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <param name="message">The error message, if any to add to the exception.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="target"/> is false.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void IsTrue(bool target, string parameterName, string message)
|
||||
{
|
||||
if (target)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentException(message, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies, that the method parameter with specified target value is false
|
||||
/// and throws an exception if it is found to be so.
|
||||
/// </summary>
|
||||
/// <param name="target">The target value, which cannot be true.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <param name="message">The error message, if any to add to the exception.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="target"/> is true.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void IsFalse(bool target, string parameterName, string message)
|
||||
{
|
||||
if (!target)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentException(message, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies, that the `source` span has the length of 'minLength', or longer.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type of the spans.</typeparam>
|
||||
/// <param name="source">The source span.</param>
|
||||
/// <param name="minLength">The minimum length.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="source"/> has less than <paramref name="minLength"/> items.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeSizedAtLeast<T>(ReadOnlySpan<T> source, int minLength, string parameterName)
|
||||
{
|
||||
if (source.Length >= minLength)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeSizedAtLeast(minLength, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies, that the `source` span has the length of 'minLength', or longer.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type of the spans.</typeparam>
|
||||
/// <param name="source">The target span.</param>
|
||||
/// <param name="minLength">The minimum length.</param>
|
||||
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// <paramref name="source"/> has less than <paramref name="minLength"/> items.
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MustBeSizedAtLeast<T>(Span<T> source, int minLength, string parameterName)
|
||||
{
|
||||
if (source.Length >= minLength)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionForMustBeSizedAtLeast(minLength, parameterName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the 'destination' span is not shorter than 'source'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The source element type.</typeparam>
|
||||
/// <typeparam name="TDest">The destination element type.</typeparam>
|
||||
/// <param name="source">The source span.</param>
|
||||
/// <param name="destination">The destination span.</param>
|
||||
/// <param name="destinationParamName">The name of the argument for 'destination'.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void DestinationShouldNotBeTooShort<TSource, TDest>(
|
||||
ReadOnlySpan<TSource> source,
|
||||
Span<TDest> destination,
|
||||
string destinationParamName)
|
||||
{
|
||||
if (destination.Length >= source.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentException("Destination span is too short!", destinationParamName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the 'destination' span is not shorter than 'source'.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The source element type.</typeparam>
|
||||
/// <typeparam name="TDest">The destination element type.</typeparam>
|
||||
/// <param name="source">The source span.</param>
|
||||
/// <param name="destination">The destination span.</param>
|
||||
/// <param name="destinationParamName">The name of the argument for 'destination'.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void DestinationShouldNotBeTooShort<TSource, TDest>(
|
||||
Span<TSource> source,
|
||||
Span<TDest> destination,
|
||||
string destinationParamName)
|
||||
{
|
||||
if (destination.Length >= source.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ThrowHelper.ThrowArgumentException("Destination span is too short!", destinationParamName);
|
||||
}
|
||||
}
|
||||
}
|
||||
373
ImageSharp.Drawing/ArcLineSegment.cs
Normal file
373
ImageSharp.Drawing/ArcLineSegment.cs
Normal file
@ -0,0 +1,373 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents a line segment that contains radii and angles that will be rendered as a elliptical arc.
|
||||
/// </summary>
|
||||
public class ArcLineSegment : ILineSegment
|
||||
{
|
||||
private const float ZeroTolerance = 1e-05F;
|
||||
private readonly PointF[] linePoints;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArcLineSegment"/> class.
|
||||
/// </summary>
|
||||
/// <param name="from">The absolute coordinates of the current point on the path.</param>
|
||||
/// <param name="to">The absolute coordinates of the final point of the arc.</param>
|
||||
/// <param name="radius">The radii of the ellipse (also known as its semi-major and semi-minor axes).</param>
|
||||
/// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param>
|
||||
/// <param name="largeArc">
|
||||
/// The large arc flag, and is <see langword="false"/> if an arc spanning less than or equal to 180 degrees
|
||||
/// is chosen, or <see langword="true"/> if an arc spanning greater than 180 degrees is chosen.
|
||||
/// </param>
|
||||
/// <param name="sweep">
|
||||
/// The sweep flag, and is <see langword="false"/> if the line joining center to arc sweeps through decreasing
|
||||
/// angles, or <see langword="true"/> if it sweeps through increasing angles.
|
||||
/// </param>
|
||||
public ArcLineSegment(PointF from, PointF to, SizeF radius, float rotation, bool largeArc, bool sweep)
|
||||
{
|
||||
rotation = GeometryUtilities.DegreeToRadian(rotation);
|
||||
bool ellipse = largeArc && ((Vector2)to - (Vector2)from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0;
|
||||
if (ellipse)
|
||||
{
|
||||
// The circle always has a start angle of 0 which is positioned at 3 o'clock.
|
||||
// This means the centre point is to the left of the start position.
|
||||
Vector2 center = (Vector2)from - new Vector2(radius.Width, 0);
|
||||
this.linePoints = EllipticArcToBezierCurve(from, center, radius, rotation, 0, sweep ? 2 * MathF.PI : -2 * MathF.PI);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep);
|
||||
}
|
||||
|
||||
this.Bounds = CalculateBounds(this.linePoints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArcLineSegment"/> class.
|
||||
/// </summary>
|
||||
/// <param name="center">The coordinates of the center of the ellipse.</param>
|
||||
/// <param name="radius">The radii of the ellipse (also known as its semi-major and semi-minor axes).</param>
|
||||
/// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param>
|
||||
/// <param name="startAngle">
|
||||
/// The start angle of the elliptical arc prior to the stretch and rotate operations.
|
||||
/// (0 is at the 3 o'clock position of the arc's circle).
|
||||
/// </param>
|
||||
/// <param name="sweepAngle">The angle between <paramref name="startAngle"/> and the end of the arc.</param>
|
||||
public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
{
|
||||
rotation = GeometryUtilities.DegreeToRadian(rotation);
|
||||
startAngle = GeometryUtilities.DegreeToRadian(Clamp(startAngle, -360F, 360F));
|
||||
sweepAngle = GeometryUtilities.DegreeToRadian(Clamp(sweepAngle, -360F, 360F));
|
||||
|
||||
Vector2 from = EllipticArcPoint(center, radius, rotation, startAngle);
|
||||
Vector2 to = EllipticArcPoint(center, radius, rotation, startAngle + sweepAngle);
|
||||
|
||||
bool largeArc = Math.Abs(sweepAngle) > MathF.PI;
|
||||
bool sweep = sweepAngle > 0;
|
||||
bool ellipse = largeArc && (to - from).LengthSquared() < ZeroTolerance && radius.Width > 0 && radius.Height > 0;
|
||||
|
||||
if (ellipse)
|
||||
{
|
||||
this.linePoints = EllipticArcToBezierCurve(from, center, radius, rotation, startAngle, sweepAngle);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep);
|
||||
}
|
||||
|
||||
this.Bounds = CalculateBounds(this.linePoints);
|
||||
}
|
||||
|
||||
private ArcLineSegment(PointF[] linePoints)
|
||||
{
|
||||
this.linePoints = linePoints;
|
||||
this.Bounds = CalculateBounds(linePoints);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PointF StartPoint => this.linePoints[0];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PointF EndPoint => this.linePoints[^1];
|
||||
|
||||
/// <inheritdoc />
|
||||
public RectangleF Bounds { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int LinearVertexCount(Vector2 scale) => this.linePoints.Length;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CopyTo(Span<PointF> destination, bool skipFirstPoint, Vector2 scale)
|
||||
{
|
||||
int startIndex = skipFirstPoint ? 1 : 0;
|
||||
ReadOnlySpan<PointF> source = this.linePoints.AsSpan(startIndex);
|
||||
|
||||
if (scale == Vector2.One)
|
||||
{
|
||||
source.CopyTo(destination);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
destination[i] = new PointF(source[i].X * scale.X, source[i].Y * scale.Y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the current <see cref="ArcLineSegment"/> using specified matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The transformation matrix.</param>
|
||||
/// <returns>An <see cref="ArcLineSegment"/> with the matrix applied to it.</returns>
|
||||
public ILineSegment Transform(Matrix4x4 matrix)
|
||||
{
|
||||
if (matrix.IsIdentity)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
PointF[] transformedPoints = new PointF[this.linePoints.Length];
|
||||
for (int i = 0; i < this.linePoints.Length; i++)
|
||||
{
|
||||
transformedPoints[i] = PointF.Transform(this.linePoints[i], matrix);
|
||||
}
|
||||
|
||||
return new ArcLineSegment(transformedPoints);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the bounds for the retained linearized arc points.
|
||||
/// </summary>
|
||||
private static RectangleF CalculateBounds(ReadOnlySpan<PointF> points)
|
||||
{
|
||||
float minX = float.MaxValue;
|
||||
float minY = float.MaxValue;
|
||||
float maxX = float.MinValue;
|
||||
float maxY = float.MinValue;
|
||||
|
||||
for (int i = 0; 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);
|
||||
}
|
||||
|
||||
private static PointF[] EllipticArcFromEndParams(
|
||||
PointF from,
|
||||
PointF to,
|
||||
SizeF radius,
|
||||
float rotation,
|
||||
bool largeArc,
|
||||
bool sweep)
|
||||
{
|
||||
Vector2 absRadius = Vector2.Abs(radius);
|
||||
|
||||
if (EllipticArcOutOfRange(from, to, radius))
|
||||
{
|
||||
return [from, to];
|
||||
}
|
||||
|
||||
EndpointToCenterArcParams(from, to, ref absRadius, rotation, largeArc, sweep, out Vector2 center, out Vector2 angles);
|
||||
return EllipticArcToBezierCurve(from, center, absRadius, rotation, angles.X, angles.Y);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool EllipticArcOutOfRange(Vector2 from, Vector2 to, Vector2 radius)
|
||||
{
|
||||
// F.6.2 Out-of-range parameters
|
||||
radius = Vector2.Abs(radius);
|
||||
float len = (to - from).LengthSquared();
|
||||
if (len < ZeroTolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (radius.X < ZeroTolerance || radius.Y < ZeroTolerance)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static Vector2 EllipticArcDerivative(Vector2 r, float xAngle, float t)
|
||||
=> new(
|
||||
(-r.X * MathF.Cos(xAngle) * MathF.Sin(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Cos(t)),
|
||||
(-r.X * MathF.Sin(xAngle) * MathF.Sin(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Cos(t)));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static Vector2 EllipticArcPoint(Vector2 c, Vector2 r, float xAngle, float t)
|
||||
=> new(
|
||||
c.X + (r.X * MathF.Cos(xAngle) * MathF.Cos(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Sin(t)),
|
||||
c.Y + (r.X * MathF.Sin(xAngle) * MathF.Cos(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Sin(t)));
|
||||
|
||||
private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, Vector2 radius, float xAngle, float startAngle, float sweepAngle)
|
||||
{
|
||||
float s = startAngle;
|
||||
float e = s + sweepAngle;
|
||||
bool neg = e < s;
|
||||
float sign = neg ? -1 : 1;
|
||||
float remain = Math.Abs(e - s);
|
||||
int curveCount = Math.Max((int)MathF.Ceiling(remain / (MathF.PI / 4F)), 1);
|
||||
|
||||
// Arc flattening retains the final point array, so use the builder to avoid the
|
||||
// intermediate collection and copy a list would generate.
|
||||
FlattenedPointBuilder points = new(curveCount * 4);
|
||||
|
||||
Vector2 prev = EllipticArcPoint(center, radius, xAngle, s);
|
||||
|
||||
while (remain > ZeroTolerance)
|
||||
{
|
||||
float step = (float)Math.Min(remain, Math.PI / 4);
|
||||
float signStep = step * sign;
|
||||
|
||||
Vector2 p1 = prev;
|
||||
Vector2 p2 = EllipticArcPoint(center, radius, xAngle, s + signStep);
|
||||
|
||||
float alphaT = (float)Math.Tan(signStep / 2);
|
||||
float alpha = (float)(Math.Sin(signStep) * (Math.Sqrt(4 + (3 * alphaT * alphaT)) - 1) / 3);
|
||||
Vector2 q1 = p1 + (alpha * EllipticArcDerivative(radius, xAngle, s));
|
||||
Vector2 q2 = p2 - (alpha * EllipticArcDerivative(radius, xAngle, s + signStep));
|
||||
|
||||
CubicBezierLineSegment bezier = new(from, q1, q2, p2);
|
||||
int bezierCount = bezier.LinearVertexCount(Vector2.One);
|
||||
Span<PointF> destination = points.GetAppendSpan(bezierCount);
|
||||
bezier.CopyTo(destination, skipFirstPoint: false, Vector2.One);
|
||||
points.Advance(bezierCount);
|
||||
|
||||
from = p2;
|
||||
|
||||
s += signStep;
|
||||
remain -= step;
|
||||
prev = p2;
|
||||
}
|
||||
|
||||
return points.Detach();
|
||||
}
|
||||
|
||||
private static void EndpointToCenterArcParams(
|
||||
Vector2 p1,
|
||||
Vector2 p2,
|
||||
ref Vector2 r,
|
||||
float xRotation,
|
||||
bool flagA,
|
||||
bool flagS,
|
||||
out Vector2 center,
|
||||
out Vector2 angles)
|
||||
{
|
||||
double rX = Math.Abs(r.X);
|
||||
double rY = Math.Abs(r.Y);
|
||||
|
||||
// (F.6.5.1)
|
||||
double dx2 = (p1.X - p2.X) / 2.0;
|
||||
double dy2 = (p1.Y - p2.Y) / 2.0;
|
||||
double x1p = (Math.Cos(xRotation) * dx2) + (Math.Sin(xRotation) * dy2);
|
||||
double y1p = (-Math.Sin(xRotation) * dx2) + (Math.Cos(xRotation) * dy2);
|
||||
|
||||
// (F.6.5.2)
|
||||
double rxs = rX * rX;
|
||||
double rys = rY * rY;
|
||||
double x1ps = x1p * x1p;
|
||||
double y1ps = y1p * y1p;
|
||||
|
||||
// check if the radius is too small `pq < 0`, when `dq > rxs * rys` (see below)
|
||||
// cr is the ratio (dq : rxs * rys)
|
||||
double cr = (x1ps / rxs) + (y1ps / rys);
|
||||
if (cr > 1)
|
||||
{
|
||||
// scale up rX,rY equally so cr == 1
|
||||
double s = Math.Sqrt(cr);
|
||||
rX = s * rX;
|
||||
rY = s * rY;
|
||||
rxs = rX * rX;
|
||||
rys = rY * rY;
|
||||
}
|
||||
|
||||
double dq = (rxs * y1ps) + (rys * x1ps);
|
||||
double pq = ((rxs * rys) - dq) / dq;
|
||||
double q = Math.Sqrt(Math.Max(0, pq)); // Use Max to account for float precision
|
||||
if (flagA == flagS)
|
||||
{
|
||||
q = -q;
|
||||
}
|
||||
|
||||
double cxp = q * rX * y1p / rY;
|
||||
double cyp = -q * rY * x1p / rX;
|
||||
|
||||
// (F.6.5.3)
|
||||
double cx = (Math.Cos(xRotation) * cxp) - (Math.Sin(xRotation) * cyp) + ((p1.X + p2.X) / 2);
|
||||
double cy = (Math.Sin(xRotation) * cxp) + (Math.Cos(xRotation) * cyp) + ((p1.Y + p2.Y) / 2);
|
||||
|
||||
// (F.6.5.5)
|
||||
double theta = SvgAngle(1, 0, (x1p - cxp) / rX, (y1p - cyp) / rY);
|
||||
|
||||
// (F.6.5.6)
|
||||
double delta = SvgAngle((x1p - cxp) / rX, (y1p - cyp) / rY, (-x1p - cxp) / rX, (-y1p - cyp) / rY);
|
||||
delta %= Math.PI * 2;
|
||||
|
||||
if (!flagS && delta > 0)
|
||||
{
|
||||
delta -= 2 * Math.PI;
|
||||
}
|
||||
|
||||
if (flagS && delta < 0)
|
||||
{
|
||||
delta += 2 * Math.PI;
|
||||
}
|
||||
|
||||
r = new Vector2((float)rX, (float)rY);
|
||||
center = new Vector2((float)cx, (float)cy);
|
||||
angles = new Vector2((float)theta, (float)delta);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static float Clamp(float val, float min, float max)
|
||||
{
|
||||
if (val < min)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
else if (val > max)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
else
|
||||
{
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static float SvgAngle(double ux, double uy, double vx, double vy)
|
||||
{
|
||||
Vector2 u = new((float)ux, (float)uy);
|
||||
Vector2 v = new((float)vx, (float)vy);
|
||||
|
||||
// (F.6.5.4)
|
||||
float dot = Vector2.Dot(u, v);
|
||||
float len = u.Length() * v.Length();
|
||||
float ang = (float)Math.Acos(Clamp(dot / len, -1, 1)); // floating point precision, slightly over values appear
|
||||
if (((u.X * v.Y) - (u.Y * v.X)) < 0)
|
||||
{
|
||||
ang = -ang;
|
||||
}
|
||||
|
||||
return ang;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
ImageSharp.Drawing/BooleanOperation.cs
Normal file
20
ImageSharp.Drawing/BooleanOperation.cs
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <inheritdoc cref="PolygonClipper.BooleanOperation" />
|
||||
public enum BooleanOperation
|
||||
{
|
||||
/// <inheritdoc cref="PolygonClipper.BooleanOperation.Intersection" />
|
||||
Intersection = 0,
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.BooleanOperation.Union" />
|
||||
Union = 1,
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.BooleanOperation.Difference" />
|
||||
Difference = 2,
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.BooleanOperation.Xor" />
|
||||
Xor = 3
|
||||
}
|
||||
}
|
||||
60
ImageSharp.Drawing/ClipPathExtensions.cs
Normal file
60
ImageSharp.Drawing/ClipPathExtensions.cs
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Drawing.PolygonGeometry;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Provides extension methods to <see cref="IPath"/> that allow the clipping of shapes.
|
||||
/// </summary>
|
||||
public static class ClipPathExtensions
|
||||
{
|
||||
private static readonly ShapeOptions DefaultOptions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Clips the specified subject path with the provided clipping paths.
|
||||
/// </summary>
|
||||
/// <param name="subjectPath">The subject path.</param>
|
||||
/// <param name="clipPaths">The clipping paths.</param>
|
||||
/// <returns>The clipped <see cref="IPath"/>.</returns>
|
||||
public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths)
|
||||
=> subjectPath.Clip(DefaultOptions, clipPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Clips the specified subject path with the provided clipping paths.
|
||||
/// </summary>
|
||||
/// <param name="subjectPath">The subject path.</param>
|
||||
/// <param name="options">The shape options.</param>
|
||||
/// <param name="clipPaths">The clipping paths.</param>
|
||||
/// <returns>The clipped <see cref="IPath"/>.</returns>
|
||||
public static IPath Clip(
|
||||
this IPath subjectPath,
|
||||
ShapeOptions options,
|
||||
params IPath[] clipPaths)
|
||||
=> ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Clips the specified subject path with the provided clipping paths.
|
||||
/// </summary>
|
||||
/// <param name="subjectPath">The subject path.</param>
|
||||
/// <param name="clipPaths">The clipping paths.</param>
|
||||
/// <returns>The clipped <see cref="IPath"/>.</returns>
|
||||
public static IPath Clip(this IPath subjectPath, IEnumerable<IPath> clipPaths)
|
||||
=> subjectPath.Clip(DefaultOptions, clipPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Clips the specified subject path with the provided clipping paths.
|
||||
/// </summary>
|
||||
/// <param name="subjectPath">The subject path.</param>
|
||||
/// <param name="options">The shape options.</param>
|
||||
/// <param name="clipPaths">The clipping paths.</param>
|
||||
/// <returns>The clipped <see cref="IPath"/>.</returns>
|
||||
public static IPath Clip(
|
||||
this IPath subjectPath,
|
||||
ShapeOptions options,
|
||||
IEnumerable<IPath> clipPaths)
|
||||
=> ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths);
|
||||
}
|
||||
}
|
||||
303
ImageSharp.Drawing/ComplexPolygon.cs
Normal file
303
ImageSharp.Drawing/ComplexPolygon.cs
Normal file
@ -0,0 +1,303 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents a complex polygon made up of one or more shapes overlayed on each other,
|
||||
/// where overlaps causes holes.
|
||||
/// </summary>
|
||||
/// <seealso cref="IPath" />
|
||||
public sealed class ComplexPolygon : IPath, IPathInternals, IInternalPathOwner
|
||||
{
|
||||
private readonly IPath[] paths;
|
||||
private List<InternalPath>? internalPaths;
|
||||
private float length;
|
||||
private RectangleF? bounds;
|
||||
private IPath? closedPath;
|
||||
private LinearGeometryCache geometryCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ComplexPolygon"/> class.
|
||||
/// </summary>
|
||||
/// <param name="contour">The contour path.</param>
|
||||
/// <param name="hole">The hole path.</param>
|
||||
public ComplexPolygon(PointF[] contour, PointF[] hole)
|
||||
: this(new Path(new LinearLineSegment(contour)), new Path(new LinearLineSegment(hole)))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ComplexPolygon" /> class.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths.</param>
|
||||
public ComplexPolygon(IEnumerable<IPath> paths)
|
||||
: this([.. paths])
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ComplexPolygon" /> class.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths.</param>
|
||||
public ComplexPolygon(params IPath[] paths)
|
||||
{
|
||||
Guard.NotNull(paths, nameof(paths));
|
||||
|
||||
this.paths = paths;
|
||||
|
||||
if (paths.Length == 0)
|
||||
{
|
||||
this.bounds = RectangleF.Empty;
|
||||
}
|
||||
|
||||
this.PathType = PathTypes.Mixed;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PathTypes PathType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of paths that make up this shape.
|
||||
/// </summary>
|
||||
public IEnumerable<IPath> Paths => this.paths;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public RectangleF Bounds => this.bounds ??= this.CalcBounds();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPath Transform(Matrix4x4 matrix)
|
||||
{
|
||||
if (matrix.IsIdentity)
|
||||
{
|
||||
// No transform to apply skip it
|
||||
return this;
|
||||
}
|
||||
|
||||
IPath[] shapes = new IPath[this.paths.Length];
|
||||
|
||||
for (int i = 0; i < shapes.Length; i++)
|
||||
{
|
||||
shapes[i] = this.paths[i].Transform(matrix);
|
||||
}
|
||||
|
||||
return new ComplexPolygon(shapes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ISimplePath> Flatten()
|
||||
{
|
||||
List<ISimplePath> paths = new(this.paths.Length);
|
||||
foreach (IPath path in this.Paths)
|
||||
{
|
||||
paths.AddRange(path.Flatten());
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public LinearGeometry ToLinearGeometry(Vector2 scale)
|
||||
=> this.geometryCache.TryGet(scale, out LinearGeometry? hit)
|
||||
? hit
|
||||
: this.geometryCache.Store(scale, this.BuildLinearGeometry(scale));
|
||||
|
||||
private LinearGeometry BuildLinearGeometry(Vector2 scale)
|
||||
{
|
||||
int pointCount = 0;
|
||||
int contourCount = 0;
|
||||
int segmentCount = 0;
|
||||
int nonHorizontalSegmentCountPixelBoundary = 0;
|
||||
int nonHorizontalSegmentCountPixelCenter = 0;
|
||||
|
||||
bool hasBounds = false;
|
||||
float minX = float.MaxValue;
|
||||
float minY = float.MaxValue;
|
||||
float maxX = float.MinValue;
|
||||
float maxY = float.MinValue;
|
||||
|
||||
foreach (IPath path in this.paths)
|
||||
{
|
||||
LinearGeometry geometry = path.ToLinearGeometry(scale);
|
||||
|
||||
if (geometry.Info.PointCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
RectangleF childBounds = geometry.Info.Bounds;
|
||||
minX = MathF.Min(minX, childBounds.Left);
|
||||
minY = MathF.Min(minY, childBounds.Top);
|
||||
maxX = MathF.Max(maxX, childBounds.Right);
|
||||
maxY = MathF.Max(maxY, childBounds.Bottom);
|
||||
hasBounds = true;
|
||||
|
||||
pointCount += geometry.Info.PointCount;
|
||||
contourCount += geometry.Info.ContourCount;
|
||||
segmentCount += geometry.Info.SegmentCount;
|
||||
nonHorizontalSegmentCountPixelBoundary += geometry.Info.NonHorizontalSegmentCountPixelBoundary;
|
||||
nonHorizontalSegmentCountPixelCenter += geometry.Info.NonHorizontalSegmentCountPixelCenter;
|
||||
}
|
||||
|
||||
PointF[] points = new PointF[pointCount];
|
||||
LinearContour[] contours = new LinearContour[contourCount];
|
||||
int pointStart = 0;
|
||||
int contourStart = 0;
|
||||
int segmentStart = 0;
|
||||
|
||||
foreach (IPath path in this.paths)
|
||||
{
|
||||
LinearGeometry geometry = path.ToLinearGeometry(scale);
|
||||
if (geometry.Info.PointCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < geometry.Points.Count; i++)
|
||||
{
|
||||
points[pointStart + i] = geometry.Points[i];
|
||||
}
|
||||
|
||||
for (int i = 0; i < geometry.Contours.Count; i++)
|
||||
{
|
||||
LinearContour contour = geometry.Contours[i];
|
||||
contours[contourStart + i] = new LinearContour
|
||||
{
|
||||
PointStart = pointStart + contour.PointStart,
|
||||
PointCount = contour.PointCount,
|
||||
SegmentStart = segmentStart + contour.SegmentStart,
|
||||
SegmentCount = contour.SegmentCount,
|
||||
IsClosed = contour.IsClosed
|
||||
};
|
||||
}
|
||||
|
||||
pointStart += geometry.Info.PointCount;
|
||||
contourStart += geometry.Info.ContourCount;
|
||||
segmentStart += geometry.Info.SegmentCount;
|
||||
}
|
||||
|
||||
RectangleF bounds = hasBounds ? RectangleF.FromLTRB(minX, minY, maxX, maxY) : RectangleF.Empty;
|
||||
|
||||
return new LinearGeometry(
|
||||
new LinearGeometryInfo
|
||||
{
|
||||
Bounds = bounds,
|
||||
ContourCount = contours.Length,
|
||||
PointCount = points.Length,
|
||||
SegmentCount = segmentCount,
|
||||
NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary,
|
||||
NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter
|
||||
},
|
||||
contours,
|
||||
points);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPath AsClosedPath()
|
||||
{
|
||||
if (this.PathType == PathTypes.Closed)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
if (this.closedPath is not null)
|
||||
{
|
||||
return this.closedPath;
|
||||
}
|
||||
|
||||
IPath[] paths = new IPath[this.paths.Length];
|
||||
for (int i = 0; i < this.paths.Length; i++)
|
||||
{
|
||||
paths[i] = this.paths[i].AsClosedPath();
|
||||
}
|
||||
|
||||
this.closedPath = new ComplexPolygon(paths);
|
||||
return this.closedPath;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
SegmentInfo IPathInternals.PointAlongPath(float distance)
|
||||
{
|
||||
this.EnsureInternalPaths();
|
||||
|
||||
distance %= this.length;
|
||||
foreach (InternalPath p in this.internalPaths)
|
||||
{
|
||||
if (p.Length >= distance)
|
||||
{
|
||||
return p.PointAlongPath(distance);
|
||||
}
|
||||
|
||||
// Reduce it before trying the next path
|
||||
distance -= p.Length;
|
||||
}
|
||||
|
||||
ThrowOutOfRange();
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
IReadOnlyList<InternalPath> IInternalPathOwner.GetRingsAsInternalPath()
|
||||
{
|
||||
this.EnsureInternalPaths();
|
||||
return this.internalPaths;
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(internalPaths))]
|
||||
private void EnsureInternalPaths()
|
||||
{
|
||||
if (this.internalPaths is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.InitInternalPaths();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes <see cref="internalPaths"/> and <see cref="length"/>.
|
||||
/// </summary>
|
||||
[MemberNotNull(nameof(internalPaths))]
|
||||
private void InitInternalPaths()
|
||||
{
|
||||
this.internalPaths = new List<InternalPath>(this.paths.Length);
|
||||
this.length = 0;
|
||||
|
||||
foreach (IPath p in this.paths)
|
||||
{
|
||||
foreach (ISimplePath s in p.Flatten())
|
||||
{
|
||||
InternalPath ip = new(s.Points, s.IsClosed);
|
||||
this.length += ip.Length;
|
||||
this.internalPaths.Add(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RectangleF CalcBounds()
|
||||
{
|
||||
float minX = float.MaxValue;
|
||||
float maxX = float.MinValue;
|
||||
float minY = float.MaxValue;
|
||||
float maxY = float.MinValue;
|
||||
|
||||
foreach (IPath p in this.paths)
|
||||
{
|
||||
RectangleF pBounds = p.Bounds;
|
||||
|
||||
minX = MathF.Min(minX, pBounds.Left);
|
||||
maxX = MathF.Max(maxX, pBounds.Right);
|
||||
minY = MathF.Min(minY, pBounds.Top);
|
||||
maxY = MathF.Max(maxY, pBounds.Bottom);
|
||||
}
|
||||
|
||||
return new RectangleF(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
private static InvalidOperationException ThrowOutOfRange() => new("Should not be possible to reach this line");
|
||||
}
|
||||
}
|
||||
263
ImageSharp.Drawing/CubicBezierLineSegment.cs
Normal file
263
ImageSharp.Drawing/CubicBezierLineSegment.cs
Normal file
@ -0,0 +1,263 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using SixLabors.ImageSharp.Drawing.Helpers;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents a line segment that contains a lists of control points that will be rendered as a cubic bezier curve
|
||||
/// </summary>
|
||||
/// <seealso cref="ILineSegment" />
|
||||
public sealed class CubicBezierLineSegment : ILineSegment
|
||||
{
|
||||
// Code for this taken from <see href="http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/"/>
|
||||
private const float MinimumSqrDistance = 1.75f;
|
||||
private const float DivisionThreshold = -.9995f;
|
||||
|
||||
private readonly PointF[] controlPoints;
|
||||
private FlattenedCache? flattenedCache;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CubicBezierLineSegment"/> class.
|
||||
/// </summary>
|
||||
/// <param name="points">The points.</param>
|
||||
public CubicBezierLineSegment(PointF[] points)
|
||||
{
|
||||
Guard.NotNull(points, nameof(points));
|
||||
Guard.MustBeGreaterThanOrEqualTo(points.Length, 4, nameof(points));
|
||||
Guard.IsTrue((points.Length - 1) % 3 == 0, nameof(points), "points must be a multiple of 3 plus 1 long.");
|
||||
this.controlPoints = points;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CubicBezierLineSegment"/> class.
|
||||
/// </summary>
|
||||
/// <param name="start">The start.</param>
|
||||
/// <param name="controlPoint1">The control point1.</param>
|
||||
/// <param name="controlPoint2">The control point2.</param>
|
||||
/// <param name="end">The end.</param>
|
||||
/// <param name="additionalPoints">The additional points.</param>
|
||||
public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end, params PointF[] additionalPoints)
|
||||
: this(new[] { start, controlPoint1, controlPoint2, end }.Concat(additionalPoints))
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="CubicBezierLineSegment(PointF, PointF, PointF, PointF, PointF[])" />
|
||||
public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end)
|
||||
: this([start, controlPoint1, controlPoint2, end])
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the control points.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PointF> ControlPoints => this.controlPoints;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PointF StartPoint => this.controlPoints[0];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PointF EndPoint => this.controlPoints[^1];
|
||||
|
||||
/// <inheritdoc />
|
||||
public RectangleF Bounds => CalculateBounds(this.GetFlattenedPoints(Vector2.One));
|
||||
|
||||
/// <inheritdoc />
|
||||
public int LinearVertexCount(Vector2 scale) => this.GetFlattenedPoints(scale).Length;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CopyTo(Span<PointF> destination, bool skipFirstPoint, Vector2 scale)
|
||||
{
|
||||
PointF[] flattened = this.GetFlattenedPoints(scale);
|
||||
int startIndex = skipFirstPoint ? 1 : 0;
|
||||
flattened.AsSpan(startIndex).CopyTo(destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the flattened point run for this curve under <paramref name="scale"/>, computing it on first
|
||||
/// request and reusing the cached result for subsequent calls at the same scale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Publication uses <see cref="Volatile.Write{T}(ref T, T)"/> so a concurrent reader either observes
|
||||
/// <see langword="null"/> or a fully-constructed entry.
|
||||
/// </remarks>
|
||||
private PointF[] GetFlattenedPoints(Vector2 scale)
|
||||
{
|
||||
FlattenedCache? hit = Volatile.Read(ref this.flattenedCache);
|
||||
if (hit is not null && hit.Scale == scale)
|
||||
{
|
||||
return hit.Points;
|
||||
}
|
||||
|
||||
PointF[] baked = FlattenCurve(this.controlPoints, scale);
|
||||
Volatile.Write(ref this.flattenedCache, new FlattenedCache(scale, baked));
|
||||
return baked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the control points of this curve.
|
||||
/// </summary>
|
||||
/// <returns>The control points of this curve.</returns>
|
||||
public ReadOnlyMemory<PointF> GetControlPoints() => this.controlPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms this line segment using the specified matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The matrix.</param>
|
||||
/// <returns>A line segment with the matrix applied to it.</returns>
|
||||
public CubicBezierLineSegment Transform(Matrix4x4 matrix)
|
||||
{
|
||||
if (matrix.IsIdentity)
|
||||
{
|
||||
// no transform to apply skip it
|
||||
return this;
|
||||
}
|
||||
|
||||
PointF[] transformedPoints = new PointF[this.controlPoints.Length];
|
||||
|
||||
for (int i = 0; i < this.controlPoints.Length; i++)
|
||||
{
|
||||
transformedPoints[i] = PointF.Transform(this.controlPoints[i], matrix);
|
||||
}
|
||||
|
||||
return new CubicBezierLineSegment(transformedPoints);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix);
|
||||
|
||||
/// <summary>
|
||||
/// Flattens every cubic in <paramref name="controlPoints"/> under the supplied device-space
|
||||
/// <paramref name="scale"/> into a single contiguous point run. Subdivision density is evaluated
|
||||
/// against the scaled control points so the polyline adapts to rendering scale.
|
||||
/// </summary>
|
||||
private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale)
|
||||
{
|
||||
int curveCount = (controlPoints.Length - 1) / 3;
|
||||
|
||||
// Flattened points are cached as a retained array, so use the builder to avoid
|
||||
// the intermediate collection and copy a list would generate.
|
||||
FlattenedPointBuilder output = new(curveCount * 4);
|
||||
|
||||
for (int curveIndex = 0; curveIndex < curveCount; curveIndex++)
|
||||
{
|
||||
int nodeIndex = curveIndex * 3;
|
||||
Vector2 p0 = new(controlPoints[nodeIndex].X * scale.X, controlPoints[nodeIndex].Y * scale.Y);
|
||||
Vector2 p1 = new(controlPoints[nodeIndex + 1].X * scale.X, controlPoints[nodeIndex + 1].Y * scale.Y);
|
||||
Vector2 p2 = new(controlPoints[nodeIndex + 2].X * scale.X, controlPoints[nodeIndex + 2].Y * scale.Y);
|
||||
Vector2 p3 = new(controlPoints[nodeIndex + 3].X * scale.X, controlPoints[nodeIndex + 3].Y * scale.Y);
|
||||
|
||||
if (curveIndex == 0)
|
||||
{
|
||||
output.Add((PointF)p0);
|
||||
}
|
||||
|
||||
SubdivideAndAppend(0F, 1F, p0, p1, p2, p3, ref output, 0);
|
||||
output.Add((PointF)p3);
|
||||
}
|
||||
|
||||
return output.Detach();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively subdivides the scaled cubic segment, appending midpoints in left-to-right order.
|
||||
/// </summary>
|
||||
private static void SubdivideAndAppend(
|
||||
float t0,
|
||||
float t1,
|
||||
Vector2 p0,
|
||||
Vector2 p1,
|
||||
Vector2 p2,
|
||||
Vector2 p3,
|
||||
ref FlattenedPointBuilder output,
|
||||
int depth)
|
||||
{
|
||||
if (depth > 999)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 left = CalculateBezierPoint(t0, p0, p1, p2, p3);
|
||||
Vector2 right = CalculateBezierPoint(t1, p0, p1, p2, p3);
|
||||
|
||||
if ((left - right).LengthSquared() < MinimumSqrDistance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float midT = (t0 + t1) / 2;
|
||||
Vector2 mid = CalculateBezierPoint(midT, p0, p1, p2, p3);
|
||||
|
||||
Vector2 leftDirection = Vector2.Normalize(left - mid);
|
||||
Vector2 rightDirection = Vector2.Normalize(right - mid);
|
||||
|
||||
if (Vector2.Dot(leftDirection, rightDirection) > DivisionThreshold || Math.Abs(midT - 0.5f) < 0.0001f)
|
||||
{
|
||||
SubdivideAndAppend(t0, midT, p0, p1, p2, p3, ref output, depth + 1);
|
||||
output.Add((PointF)mid);
|
||||
SubdivideAndAppend(midT, t1, p0, p1, p2, p3, ref output, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the bezier point along the line.
|
||||
/// </summary>
|
||||
/// <param name="t">The position within the line.</param>
|
||||
/// <param name="p0">The p 0.</param>
|
||||
/// <param name="p1">The p 1.</param>
|
||||
/// <param name="p2">The p 2.</param>
|
||||
/// <param name="p3">The p 3.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="Vector2"/>.
|
||||
/// </returns>
|
||||
private static Vector2 CalculateBezierPoint(float t, Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3)
|
||||
{
|
||||
float u = 1 - t;
|
||||
float tt = t * t;
|
||||
float uu = u * u;
|
||||
float uuu = uu * u;
|
||||
float ttt = tt * t;
|
||||
|
||||
Vector2 p = uuu * p0; // first term
|
||||
|
||||
p += 3 * uu * t * p1; // second term
|
||||
p += 3 * u * tt * p2; // third term
|
||||
p += ttt * p3; // fourth term
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the bounds for the cached linearized bezier points.
|
||||
/// </summary>
|
||||
private static RectangleF CalculateBounds(ReadOnlySpan<PointF> points)
|
||||
{
|
||||
float minX = float.MaxValue;
|
||||
float minY = float.MaxValue;
|
||||
float maxX = float.MinValue;
|
||||
float maxY = float.MinValue;
|
||||
|
||||
for (int i = 0; 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);
|
||||
}
|
||||
|
||||
private sealed class FlattenedCache(Vector2 scale, PointF[] points)
|
||||
{
|
||||
public Vector2 Scale { get; } = scale;
|
||||
|
||||
public PointF[] Points { get; } = points;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
ImageSharp.Drawing/EllipsePolygon.cs
Normal file
125
ImageSharp.Drawing/EllipsePolygon.cs
Normal file
@ -0,0 +1,125 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// An elliptical shape made up of a single path made up of one of more <see cref="ILineSegment"/>s.
|
||||
/// </summary>
|
||||
public sealed class EllipsePolygon : Polygon, IPathInternals
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EllipsePolygon" /> class.
|
||||
/// </summary>
|
||||
/// <param name="location">The location the center of the ellipse will be placed.</param>
|
||||
/// <param name="size">The width/height of the final ellipse.</param>
|
||||
public EllipsePolygon(PointF location, SizeF size)
|
||||
: base(CreateSegment(location, size))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EllipsePolygon" /> class.
|
||||
/// </summary>
|
||||
/// <param name="location">The location the center of the circle will be placed.</param>
|
||||
/// <param name="radius">The radius final circle.</param>
|
||||
public EllipsePolygon(PointF location, float radius)
|
||||
: this(location, new SizeF(radius * 2, radius * 2))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EllipsePolygon" /> class.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the center of the ellipse.</param>
|
||||
/// <param name="y">The y-coordinate of the center of the ellipse.</param>
|
||||
/// <param name="width">The width the ellipse should have.</param>
|
||||
/// <param name="height">The height the ellipse should have.</param>
|
||||
public EllipsePolygon(float x, float y, float width, float height)
|
||||
: this(new PointF(x, y), new SizeF(width, height))
|
||||
{
|
||||
}
|
||||
|
||||
private EllipsePolygon(ILineSegment[] segments)
|
||||
: base(segments, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EllipsePolygon" /> class.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the center of the circle.</param>
|
||||
/// <param name="y">The y-coordinate of the center of the circle.</param>
|
||||
/// <param name="radius">The radius final circle.</param>
|
||||
public EllipsePolygon(float x, float y, float radius)
|
||||
: this(new PointF(x, y), new SizeF(radius * 2, radius * 2))
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IPath Transform(Matrix4x4 matrix)
|
||||
{
|
||||
if (matrix.IsIdentity)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
ILineSegment[] segments = new ILineSegment[this.LineSegments.Count];
|
||||
|
||||
for (int i = 0; i < segments.Length; i++)
|
||||
{
|
||||
segments[i] = this.LineSegments[i].Transform(matrix);
|
||||
}
|
||||
|
||||
return new EllipsePolygon(segments);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
// TODO switch this out to a calculated algorithm
|
||||
SegmentInfo IPathInternals.PointAlongPath(float distance)
|
||||
=> this.InnerPath.PointAlongPath(distance);
|
||||
|
||||
private static CubicBezierLineSegment CreateSegment(Vector2 location, SizeF size)
|
||||
{
|
||||
Guard.MustBeGreaterThan(size.Width, 0, "width");
|
||||
Guard.MustBeGreaterThan(size.Height, 0, "height");
|
||||
|
||||
const float kappa = 0.5522848f;
|
||||
|
||||
Vector2 sizeVector = size;
|
||||
sizeVector /= 2;
|
||||
|
||||
Vector2 rootLocation = location - sizeVector;
|
||||
|
||||
Vector2 pointO = sizeVector * kappa;
|
||||
Vector2 pointE = location + sizeVector;
|
||||
Vector2 pointM = location;
|
||||
Vector2 pointMminusO = pointM - pointO;
|
||||
Vector2 pointMplusO = pointM + pointO;
|
||||
|
||||
PointF[] points =
|
||||
[
|
||||
new Vector2(rootLocation.X, pointM.Y),
|
||||
|
||||
new Vector2(rootLocation.X, pointMminusO.Y),
|
||||
new Vector2(pointMminusO.X, rootLocation.Y),
|
||||
new Vector2(pointM.X, rootLocation.Y),
|
||||
|
||||
new Vector2(pointMplusO.X, rootLocation.Y),
|
||||
new Vector2(pointE.X, pointMminusO.Y),
|
||||
new Vector2(pointE.X, pointM.Y),
|
||||
|
||||
new Vector2(pointE.X, pointMplusO.Y),
|
||||
new Vector2(pointMplusO.X, pointE.Y),
|
||||
new Vector2(pointM.X, pointE.Y),
|
||||
|
||||
new Vector2(pointMminusO.X, pointE.Y),
|
||||
new Vector2(rootLocation.X, pointMplusO.Y),
|
||||
new Vector2(rootLocation.X, pointM.Y)
|
||||
];
|
||||
|
||||
return new CubicBezierLineSegment(points);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
ImageSharp.Drawing/EmptyPath.cs
Normal file
56
ImageSharp.Drawing/EmptyPath.cs
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// A path that is always empty.
|
||||
/// </summary>
|
||||
public sealed class EmptyPath : IPath
|
||||
{
|
||||
private static readonly LinearGeometry EmptyGeometry = new(
|
||||
new LinearGeometryInfo
|
||||
{
|
||||
Bounds = RectangleF.Empty,
|
||||
ContourCount = 0,
|
||||
PointCount = 0,
|
||||
SegmentCount = 0,
|
||||
NonHorizontalSegmentCountPixelBoundary = 0,
|
||||
NonHorizontalSegmentCountPixelCenter = 0
|
||||
},
|
||||
[],
|
||||
[]);
|
||||
|
||||
private EmptyPath(PathTypes pathType) => this.PathType = pathType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the closed path instance of the empty path
|
||||
/// </summary>
|
||||
public static EmptyPath ClosedPath { get; } = new(PathTypes.Closed);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the open path instance of the empty path
|
||||
/// </summary>
|
||||
public static EmptyPath OpenPath { get; } = new(PathTypes.Open);
|
||||
|
||||
/// <inheritdoc />
|
||||
public PathTypes PathType { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public RectangleF Bounds => RectangleF.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPath AsClosedPath() => ClosedPath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ISimplePath> Flatten() => [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public LinearGeometry ToLinearGeometry(Vector2 scale) => EmptyGeometry;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPath Transform(Matrix4x4 matrix) => this;
|
||||
}
|
||||
}
|
||||
84
ImageSharp.Drawing/FlattenedPointBuilder.cs
Normal file
84
ImageSharp.Drawing/FlattenedPointBuilder.cs
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Builds the retained <see cref="PointF"/> array used by flattened segment caches without an intermediate collection copy.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Segment flatteners ultimately need to return a tightly-sized array that can be cached by the segment instance.
|
||||
/// This builder owns that array while points are appended.
|
||||
/// </remarks>
|
||||
internal struct FlattenedPointBuilder
|
||||
{
|
||||
private PointF[] points;
|
||||
private int count;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FlattenedPointBuilder"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The estimated number of points that will be appended.</param>
|
||||
public FlattenedPointBuilder(int capacity)
|
||||
{
|
||||
this.points = new PointF[Math.Max(capacity, 4)];
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one point to the retained point array.
|
||||
/// </summary>
|
||||
/// <param name="point">The point to append.</param>
|
||||
public void Add(PointF point)
|
||||
{
|
||||
this.EnsureCapacity(this.count + 1);
|
||||
this.points[this.count++] = point;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reserves a writable append window for callers that populate multiple points directly.
|
||||
/// </summary>
|
||||
/// <param name="length">The number of points to reserve.</param>
|
||||
/// <returns>A span covering the reserved append window.</returns>
|
||||
public Span<PointF> GetAppendSpan(int length)
|
||||
{
|
||||
this.EnsureCapacity(this.count + length);
|
||||
return this.points.AsSpan(this.count, length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Commits points previously written through <see cref="GetAppendSpan"/>.
|
||||
/// </summary>
|
||||
/// <param name="length">The number of points written to the reserved append window.</param>
|
||||
public void Advance(int length) => this.count += length;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the owned point array.
|
||||
/// </summary>
|
||||
/// <returns>The tightly-sized retained point array.</returns>
|
||||
public PointF[] Detach()
|
||||
{
|
||||
if (this.count != this.points.Length)
|
||||
{
|
||||
Array.Resize(ref this.points, this.count);
|
||||
}
|
||||
|
||||
return this.points;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the owned array can store the requested total point count.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The total number of points that must fit.</param>
|
||||
private void EnsureCapacity(int capacity)
|
||||
{
|
||||
if (capacity <= this.points.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Array.Resize(ref this.points, Math.Max(capacity, this.points.Length * 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
ImageSharp.Drawing/Helpers/ArrayExtensions.cs
Normal file
36
ImageSharp.Drawing/Helpers/ArrayExtensions.cs
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Helpers {
|
||||
/// <summary>
|
||||
/// Extension methods for arrays.
|
||||
/// </summary>
|
||||
internal static class ArrayExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Concatenates two arrays into one.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The element type.</typeparam>
|
||||
/// <param name="source1">The first source array.</param>
|
||||
/// <param name="source2">The second source array.</param>
|
||||
/// <returns>
|
||||
/// A new array containing the elements of both source arrays, or <paramref name="source1"/>
|
||||
/// when <paramref name="source2"/> is empty.
|
||||
/// </returns>
|
||||
public static T[] Concat<T>(this T[] source1, T[] source2)
|
||||
{
|
||||
if (source2 is null || source2.Length == 0)
|
||||
{
|
||||
return source1;
|
||||
}
|
||||
|
||||
T[] target = new T[source1.Length + source2.Length];
|
||||
source1.AsSpan().CopyTo(target);
|
||||
source2.AsSpan().CopyTo(target.AsSpan(source1.Length));
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
ImageSharp.Drawing/Helpers/MatrixUtilities.cs
Normal file
29
ImageSharp.Drawing/Helpers/MatrixUtilities.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Helpers {
|
||||
/// <summary>
|
||||
/// Provides helper methods for extracting properties from transformation matrices.
|
||||
/// </summary>
|
||||
internal static class MatrixUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts the average 2D scale factor from a <see cref="Matrix4x4"/>.
|
||||
/// This is the mean of the X and Y axis scale magnitudes, suitable for
|
||||
/// uniformly scaling radii under non-uniform or projective transforms.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The transformation matrix.</param>
|
||||
/// <returns>The average scale factor.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static float GetAverageScale(in Matrix4x4 matrix)
|
||||
{
|
||||
float sx = MathF.Sqrt((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12));
|
||||
float sy = MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22));
|
||||
return (sx + sy) * 0.5f;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
ImageSharp.Drawing/Helpers/PolygonUtilities.cs
Normal file
126
ImageSharp.Drawing/Helpers/PolygonUtilities.cs
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Drawing;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Helpers {
|
||||
/// <summary>
|
||||
/// Provides low-level geometry helpers for polygon winding and segment intersection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Polygon methods expect a closed ring where the first point is repeated as the last point.
|
||||
/// Orientation signs are defined using world-space math conventions (Y points up):
|
||||
/// positive signed area is counter-clockwise and negative signed area is clockwise.
|
||||
/// In screen space (Y points down), the visual winding appears inverted.
|
||||
/// </remarks>
|
||||
internal static class PolygonUtilities
|
||||
{
|
||||
// Epsilon used for floating-point tolerance. Values within +-Eps are treated as zero.
|
||||
// This reduces instability when segments are nearly parallel or endpoints are close.
|
||||
private const float Eps = 1e-3f;
|
||||
private const float MinusEps = -Eps;
|
||||
private const float OnePlusEps = 1 + Eps;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that a closed polygon ring matches the expected orientation.
|
||||
/// </summary>
|
||||
/// <param name="polygon">Polygon ring to normalize in place.</param>
|
||||
/// <param name="expectedOrientation">
|
||||
/// Expected orientation sign:
|
||||
/// positive for counter-clockwise in world space, negative for clockwise in world space.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// The ring is reversed only when its orientation sign disagrees with
|
||||
/// <paramref name="expectedOrientation"/>. Degenerate rings (zero area) are not changed.
|
||||
/// </remarks>
|
||||
public static void EnsureOrientation(Span<PointF> polygon, int expectedOrientation)
|
||||
{
|
||||
if (GetPolygonOrientation(polygon) * expectedOrientation < 0)
|
||||
{
|
||||
polygon.Reverse();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the orientation sign of a closed polygon ring using the shoelace sum.
|
||||
/// </summary>
|
||||
/// <param name="polygon">Closed polygon ring.</param>
|
||||
/// <returns>
|
||||
/// -1 for clockwise, 1 for counter-clockwise, or 0 for degenerate (zero-area) input.
|
||||
/// </returns>
|
||||
private static int GetPolygonOrientation(ReadOnlySpan<PointF> polygon)
|
||||
{
|
||||
float sum = 0f;
|
||||
for (int i = 0; i < polygon.Length - 1; ++i)
|
||||
{
|
||||
PointF current = polygon[i];
|
||||
PointF next = polygon[i + 1];
|
||||
sum += (current.X * next.Y) - (next.X * current.Y);
|
||||
}
|
||||
|
||||
// A tolerant compare could be used here, but edge scanning does not special-case
|
||||
// zero-area or near-zero-area input, so we keep this strict sign check.
|
||||
return Math.Sign(sum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether two line segments intersect, excluding collinear overlap cases.
|
||||
/// </summary>
|
||||
/// <param name="a0">Start point of segment A.</param>
|
||||
/// <param name="a1">End point of segment A.</param>
|
||||
/// <param name="b0">Start point of segment B.</param>
|
||||
/// <param name="b1">End point of segment B.</param>
|
||||
/// <param name="intersectionPoint">
|
||||
/// Receives the intersection point when an intersection is found.
|
||||
/// If no intersection is detected, the value is not modified.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> when the segments intersect within their extents
|
||||
/// (including endpoints); otherwise <see langword="false"/>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This solves the two segment equations in parametric form and accepts values in [0, 1]
|
||||
/// with an epsilon margin for floating-point tolerance.
|
||||
/// Parallel and collinear pairs are rejected early (cross product ~= 0).
|
||||
/// </remarks>
|
||||
public static bool LineSegmentToLineSegmentIgnoreCollinear(
|
||||
Vector2 a0,
|
||||
Vector2 a1,
|
||||
Vector2 b0,
|
||||
Vector2 b1,
|
||||
ref Vector2 intersectionPoint)
|
||||
{
|
||||
// Direction vectors of the segments.
|
||||
float dax = a1.X - a0.X;
|
||||
float day = a1.Y - a0.Y;
|
||||
float dbx = b1.X - b0.X;
|
||||
float dby = b1.Y - b0.Y;
|
||||
|
||||
// Cross product of the direction vectors. Near zero means parallel/collinear.
|
||||
float crossD = (-dbx * day) + (dax * dby);
|
||||
|
||||
// Reject parallel and collinear lines. Collinear overlap is intentionally not handled.
|
||||
if (crossD is > MinusEps and < Eps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Solve for parameters s and t where:
|
||||
// a0 + t * (a1 - a0) = b0 + s * (b1 - b0)
|
||||
float s = ((-day * (a0.X - b0.X)) + (dax * (a0.Y - b0.Y))) / crossD;
|
||||
float t = ((dbx * (a0.Y - b0.Y)) - (dby * (a0.X - b0.X))) / crossD;
|
||||
|
||||
// If both parameters are within [0,1] (with tolerance), the segments intersect.
|
||||
if (s > MinusEps && s < OnePlusEps && t > MinusEps && t < OnePlusEps)
|
||||
{
|
||||
intersectionPoint.X = a0.X + (t * dax);
|
||||
intersectionPoint.Y = a0.Y + (t * day);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
ImageSharp.Drawing/IInternalPathOwner.cs
Normal file
19
ImageSharp.Drawing/IInternalPathOwner.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// An internal interface for shapes which are backed by <see cref="InternalPath"/>
|
||||
/// so we can have a fast path tessellating them.
|
||||
/// </summary>
|
||||
internal interface IInternalPathOwner
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the rings as a readonly collection of <see cref="InternalPath"/> elements.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IReadOnlyList{T}"/>.</returns>
|
||||
public IReadOnlyList<InternalPath> GetRingsAsInternalPath();
|
||||
}
|
||||
}
|
||||
55
ImageSharp.Drawing/ILineSegment.cs
Normal file
55
ImageSharp.Drawing/ILineSegment.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents a simple path segment
|
||||
/// </summary>
|
||||
public interface ILineSegment
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the start point.
|
||||
/// </summary>
|
||||
public PointF StartPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The end point.
|
||||
/// </value>
|
||||
public PointF EndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bounds of the linearized segment output.
|
||||
/// </summary>
|
||||
public RectangleF Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of linear vertices emitted by this segment when flattened under the supplied
|
||||
/// device-space <paramref name="scale"/>.
|
||||
/// </summary>
|
||||
/// <param name="scale">The X/Y scale at which curves are flattened. Pass <see cref="Vector2.One"/> for local-space counts.</param>
|
||||
/// <returns>The number of linear vertices this segment emits.</returns>
|
||||
public int LinearVertexCount(Vector2 scale);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the segment's linearized points to <paramref name="destination"/>, baked at the supplied
|
||||
/// device-space <paramref name="scale"/>.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination point span.</param>
|
||||
/// <param name="skipFirstPoint">Whether to skip the first emitted point.</param>
|
||||
/// <param name="scale">The X/Y scale at which curves are flattened. Pass <see cref="Vector2.One"/> for local-space output.</param>
|
||||
public void CopyTo(Span<PointF> destination, bool skipFirstPoint, Vector2 scale);
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the current LineSegment using specified matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The matrix.</param>
|
||||
/// <returns>A line segment with the matrix applied to it.</returns>
|
||||
public ILineSegment Transform(Matrix4x4 matrix);
|
||||
}
|
||||
}
|
||||
50
ImageSharp.Drawing/IPath.cs
Normal file
50
ImageSharp.Drawing/IPath.cs
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents a logic path that can be drawn.
|
||||
/// </summary>
|
||||
public interface IPath
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is closed, open or a composite path with a mixture of open and closed figures.
|
||||
/// </summary>
|
||||
public PathTypes PathType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bounds enclosing the path.
|
||||
/// </summary>
|
||||
public RectangleF Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the <see cref="IPath" /> into a simple linear path.
|
||||
/// </summary>
|
||||
/// <returns>Returns the current <see cref="IPath" /> as simple linear path.</returns>
|
||||
public IEnumerable<ISimplePath> Flatten();
|
||||
|
||||
/// <summary>
|
||||
/// Converts this path into a retained <see cref="LinearGeometry"/>, flattening curves at the precision of
|
||||
/// the supplied device-space <paramref name="scale"/>.
|
||||
/// </summary>
|
||||
/// <param name="scale">The X/Y scale at which curves are flattened.</param>
|
||||
/// <returns>The retained linear geometry.</returns>
|
||||
public LinearGeometry ToLinearGeometry(Vector2 scale);
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the path using the specified matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The matrix.</param>
|
||||
/// <returns>A new path with the matrix applied to it.</returns>
|
||||
public IPath Transform(Matrix4x4 matrix);
|
||||
|
||||
/// <summary>
|
||||
/// Returns this path with all figures closed.
|
||||
/// </summary>
|
||||
/// <returns>A new close <see cref="IPath"/>.</returns>
|
||||
public IPath AsClosedPath();
|
||||
}
|
||||
}
|
||||
25
ImageSharp.Drawing/IPathCollection.cs
Normal file
25
ImageSharp.Drawing/IPathCollection.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents a logic path that can be drawn
|
||||
/// </summary>
|
||||
public interface IPathCollection : IEnumerable<IPath>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the bounds enclosing the path
|
||||
/// </summary>
|
||||
public RectangleF Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the path using the specified matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The matrix.</param>
|
||||
/// <returns>A new path collection with the matrix applied to it.</returns>
|
||||
public IPathCollection Transform(Matrix4x4 matrix);
|
||||
}
|
||||
}
|
||||
19
ImageSharp.Drawing/IPathInternals.cs
Normal file
19
ImageSharp.Drawing/IPathInternals.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// An interface for internal operations we don't want to expose on <see cref="IPath"/>.
|
||||
/// </summary>
|
||||
internal interface IPathInternals : IPath
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns information about a point at a given distance along a path.
|
||||
/// </summary>
|
||||
/// <param name="distance">The distance along the path to return details for.</param>
|
||||
/// <returns>
|
||||
/// The segment information.
|
||||
/// </returns>
|
||||
SegmentInfo PointAlongPath(float distance);
|
||||
}
|
||||
}
|
||||
22
ImageSharp.Drawing/ISimplePath.cs
Normal file
22
ImageSharp.Drawing/ISimplePath.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents a simple (non-composite) path defined by a series of points.
|
||||
/// </summary>
|
||||
public interface ISimplePath
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is a closed path.
|
||||
/// </summary>
|
||||
public bool IsClosed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the points that make this up as a simple linear path.
|
||||
/// </summary>
|
||||
public ReadOnlyMemory<PointF> Points { get; }
|
||||
}
|
||||
}
|
||||
37
ImageSharp.Drawing/ImageSharp.Drawing.csproj
Normal file
37
ImageSharp.Drawing/ImageSharp.Drawing.csproj
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AssemblyName>SixLabors.ImageSharp.Drawing</AssemblyName>
|
||||
<AssemblyTitle>SixLabors.ImageSharp.Drawing</AssemblyTitle>
|
||||
<RootNamespace>SixLabors.ImageSharp.Drawing</RootNamespace>
|
||||
<PackageId>SixLabors.ImageSharp.Drawing</PackageId>
|
||||
<PackageIcon>sixlabors.imagesharp.drawing.128.png</PackageIcon>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<RepositoryUrl Condition="'$(RepositoryUrl)' == ''">https://github.com/SixLabors/ImageSharp.Drawing/</RepositoryUrl>
|
||||
<PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl>
|
||||
<PackageTags>ImageSharp Drawing Graphics Shapes Paths Text Fonts Vector Raster</PackageTags>
|
||||
<Description>Drawing extensions for ImageSharp with support for shapes, paths, text, and image rendering.</Description>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- This enables the nullable analysis and treats all nullable warnings as error-->
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>Nullable</WarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ImageSharp\ImageSharp.csproj" />
|
||||
<ProjectReference Include="..\PolygonClipper\PolygonClipper.csproj" />
|
||||
<ProjectReference Include="..\SixLabors.Fonts\SixLabors.Fonts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- <None Include="..\..\LICENSE" Pack="true" PackagePath="" /> -->
|
||||
<!-- <None Include="..\..\shared-infrastructure\branding\icons\imagesharp.drawing\sixlabors.imagesharp.drawing.128.png" Pack="true" PackagePath="" /> -->
|
||||
|
||||
<Import Project="..\SharedInfrastructure.projitems" Label="Shared" />
|
||||
|
||||
</Project>
|
||||
397
ImageSharp.Drawing/InternalPath.cs
Normal file
397
ImageSharp.Drawing/InternalPath.cs
Normal file
@ -0,0 +1,397 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Internal logic for integrating linear paths.
|
||||
/// </summary>
|
||||
internal class InternalPath
|
||||
{
|
||||
/// <summary>
|
||||
/// The epsilon for float comparison
|
||||
/// </summary>
|
||||
private const float Epsilon = 0.003f;
|
||||
private const float Epsilon2 = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// The points.
|
||||
/// </summary>
|
||||
private readonly PointData[] points;
|
||||
|
||||
/// <summary>
|
||||
/// Materialized points projected from <see cref="points"/>.
|
||||
/// </summary>
|
||||
private PointF[]? materializedPoints;
|
||||
|
||||
/// <summary>
|
||||
/// The closed path.
|
||||
/// </summary>
|
||||
private readonly bool closedPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InternalPath"/> class.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments.</param>
|
||||
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
|
||||
/// <param name="removeCloseAndCollinear">Whether to remove close and collinear vertices</param>
|
||||
internal InternalPath(IReadOnlyList<ILineSegment> segments, bool isClosedPath, bool removeCloseAndCollinear = true)
|
||||
: this(Simplify(segments, isClosedPath, removeCloseAndCollinear), isClosedPath)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InternalPath" /> class.
|
||||
/// </summary>
|
||||
/// <param name="points">The points.</param>
|
||||
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
|
||||
internal InternalPath(ReadOnlyMemory<PointF> points, bool isClosedPath)
|
||||
: this(Simplify(points.Span, isClosedPath, true), isClosedPath)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InternalPath" /> class.
|
||||
/// </summary>
|
||||
/// <param name="points">The points.</param>
|
||||
/// <param name="isClosedPath">if set to <c>true</c> [is closed path].</param>
|
||||
private InternalPath(PointData[] points, bool isClosedPath)
|
||||
{
|
||||
this.points = points;
|
||||
this.closedPath = isClosedPath;
|
||||
|
||||
if (this.points.Length > 0)
|
||||
{
|
||||
float minX, minY, maxX, maxY, length;
|
||||
length = 0;
|
||||
minX = minY = float.MaxValue;
|
||||
maxX = maxY = float.MinValue;
|
||||
|
||||
foreach (PointData point in this.points)
|
||||
{
|
||||
length += point.Length;
|
||||
minX = Math.Min(point.Point.X, minX);
|
||||
minY = Math.Min(point.Point.Y, minY);
|
||||
maxX = Math.Max(point.Point.X, maxX);
|
||||
maxY = Math.Max(point.Point.Y, maxY);
|
||||
}
|
||||
|
||||
this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY);
|
||||
this.Length = length;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Bounds = RectangleF.Empty;
|
||||
this.Length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bounds.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The bounds.
|
||||
/// </value>
|
||||
public RectangleF Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The length.
|
||||
/// </value>
|
||||
public float Length { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length.
|
||||
/// </summary>
|
||||
public int PointCount => this.points.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the points.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IReadOnlyCollection{PointF}"/></returns>
|
||||
internal ReadOnlyMemory<PointF> Points() => this.materializedPoints ??= this.CreatePoints();
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the point a certain distance a path.
|
||||
/// </summary>
|
||||
/// <param name="distanceAlongPath">The distance along the path to find details of.</param>
|
||||
/// <returns>
|
||||
/// Returns details about a point along a path.
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if no points found.</exception>
|
||||
internal SegmentInfo PointAlongPath(float distanceAlongPath)
|
||||
{
|
||||
int pointCount = this.PointCount;
|
||||
if (this.closedPath)
|
||||
{
|
||||
// Move the distance back to the beginning since this is a closed polygon.
|
||||
distanceAlongPath %= this.Length;
|
||||
pointCount--;
|
||||
}
|
||||
|
||||
for (int i = 0; i < pointCount; i++)
|
||||
{
|
||||
int next = WrapArrayIndex(i + 1, this.PointCount);
|
||||
if (distanceAlongPath < this.points[next].Length)
|
||||
{
|
||||
float t = distanceAlongPath / this.points[next].Length;
|
||||
Vector2 point = Vector2.Lerp(this.points[i].Point, this.points[next].Point, t);
|
||||
Vector2 diff = this.points[i].Point - this.points[next].Point;
|
||||
|
||||
return new SegmentInfo
|
||||
{
|
||||
Point = point,
|
||||
Angle = (float)(Math.Atan2(diff.Y, diff.X) % (Math.PI * 2))
|
||||
};
|
||||
}
|
||||
|
||||
distanceAlongPath -= this.points[next].Length;
|
||||
}
|
||||
|
||||
// Closed paths will never reach this point.
|
||||
// For open paths we're going to create a new virtual point that extends past the path.
|
||||
// The position and angle for that point are calculated based upon the last two points.
|
||||
PointF a = this.points[Math.Max(this.points.Length - 2, 0)].Point;
|
||||
PointF b = this.points[^1].Point;
|
||||
Vector2 delta = a - b;
|
||||
float angle = (float)(Math.Atan2(delta.Y, delta.X) % (Math.PI * 2));
|
||||
|
||||
Matrix4x4 transform = Matrix4x4.CreateRotationZ(angle - MathF.PI) * Matrix4x4.CreateTranslation(b.X, b.Y, 0);
|
||||
|
||||
return new SegmentInfo
|
||||
{
|
||||
Point = PointF.Transform(new PointF(distanceAlongPath, 0), transform),
|
||||
Angle = angle
|
||||
};
|
||||
}
|
||||
|
||||
// Modulo is a very slow operation.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int WrapArrayIndex(int i, int arrayLength) => i < arrayLength ? i : i - arrayLength;
|
||||
|
||||
private PointF[] CreatePoints()
|
||||
{
|
||||
PointF[] result = new PointF[this.points.Length];
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
{
|
||||
result[i] = this.points[i].Point;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vector2 r)
|
||||
{
|
||||
// See http://www.geeksforgeeks.org/orientation-3-ordered-points/
|
||||
// for details of below formula.
|
||||
Vector2 qp = q - p;
|
||||
Vector2 rq = r - q;
|
||||
float val = (qp.Y * rq.X) - (qp.X * rq.Y);
|
||||
|
||||
if (val is > -Epsilon and < Epsilon)
|
||||
{
|
||||
return PointOrientation.Collinear; // colinear
|
||||
}
|
||||
|
||||
return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simplifies the collection of segments.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments.</param>
|
||||
/// <param name="isClosed">Weather the path is closed or open.</param>
|
||||
/// <param name="removeCloseAndCollinear">Whether to remove close and collinear vertices</param>
|
||||
/// <returns>
|
||||
/// The <see cref="T:PointData[]"/>.
|
||||
/// </returns>
|
||||
private static PointData[] Simplify(IReadOnlyList<ILineSegment> segments, bool isClosed, bool removeCloseAndCollinear)
|
||||
{
|
||||
// Pre-compute capacity from identity-transform vertex counts to avoid List resizing.
|
||||
int totalPoints = 0;
|
||||
for (int s = 0; s < segments.Count; s++)
|
||||
{
|
||||
totalPoints += segments[s].LinearVertexCount(Vector2.One);
|
||||
}
|
||||
|
||||
List<PointF> simplified = new(totalPoints);
|
||||
|
||||
// Track indices where collinear direction reversals represent user-intended
|
||||
// geometry: interior points of multi-point linear segments, and junction
|
||||
// points between two linear segments (e.g. PathBuilder LineTo → LineTo).
|
||||
// Reversals at all other indices (flattened curves, curve junctions) are
|
||||
// artifacts and should be removed normally.
|
||||
HashSet<int>? linearReversalIndices = null;
|
||||
ILineSegment? prevSeg = null;
|
||||
|
||||
foreach (ILineSegment seg in segments)
|
||||
{
|
||||
int start = simplified.Count;
|
||||
int segmentCount = seg.LinearVertexCount(Vector2.One);
|
||||
CollectionsMarshal.SetCount(simplified, start + segmentCount);
|
||||
Span<PointF> destination = CollectionsMarshal.AsSpan(simplified).Slice(start, segmentCount);
|
||||
seg.CopyTo(destination, skipFirstPoint: false, Vector2.One);
|
||||
|
||||
if (seg is LinearLineSegment)
|
||||
{
|
||||
// Interior points of a multi-point linear segment (e.g. DrawLine with 3+ points).
|
||||
if (segmentCount > 2)
|
||||
{
|
||||
linearReversalIndices ??= [];
|
||||
for (int i = start + 1; i < start + segmentCount - 1; i++)
|
||||
{
|
||||
_ = linearReversalIndices.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Junction between two linear segments (e.g. PathBuilder LineTo → LineTo).
|
||||
if (prevSeg is LinearLineSegment && start > 0)
|
||||
{
|
||||
linearReversalIndices ??= [];
|
||||
_ = linearReversalIndices.Add(start);
|
||||
}
|
||||
}
|
||||
|
||||
prevSeg = seg;
|
||||
}
|
||||
|
||||
return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear, linearReversalIndices);
|
||||
}
|
||||
|
||||
private static PointData[] Simplify(ReadOnlySpan<PointF> points, bool isClosed, bool removeCloseAndCollinear, HashSet<int>? linearReversalIndices = null)
|
||||
{
|
||||
int polyCorners = points.Length;
|
||||
if (polyCorners == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
List<PointData> results = new(polyCorners);
|
||||
Vector2 lastPoint = points[0];
|
||||
|
||||
if (!isClosed)
|
||||
{
|
||||
results.Add(new PointData
|
||||
{
|
||||
Point = points[0],
|
||||
Orientation = PointOrientation.Collinear,
|
||||
Length = 0
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
int prev = polyCorners;
|
||||
do
|
||||
{
|
||||
prev--;
|
||||
if (prev == 0)
|
||||
{
|
||||
// All points are common, shouldn't match anything
|
||||
results.Add(
|
||||
new PointData
|
||||
{
|
||||
Point = points[0],
|
||||
Orientation = PointOrientation.Collinear,
|
||||
Length = 0,
|
||||
});
|
||||
|
||||
return [.. results];
|
||||
}
|
||||
}
|
||||
while (removeCloseAndCollinear && Equivalent(points[0], points[prev], Epsilon2)); // skip points too close together
|
||||
|
||||
polyCorners = prev + 1;
|
||||
lastPoint = points[prev];
|
||||
|
||||
results.Add(
|
||||
new PointData
|
||||
{
|
||||
Point = points[0],
|
||||
Orientation = CalculateOrientation(lastPoint, points[0], points[1]),
|
||||
Length = Vector2.Distance(lastPoint, points[0]),
|
||||
});
|
||||
|
||||
lastPoint = points[0];
|
||||
}
|
||||
|
||||
for (int i = 1; i < polyCorners; i++)
|
||||
{
|
||||
int next = WrapArrayIndex(i + 1, polyCorners);
|
||||
PointOrientation or = CalculateOrientation(lastPoint, points[i], points[next]);
|
||||
if (removeCloseAndCollinear && or == PointOrientation.Collinear && next != 0)
|
||||
{
|
||||
// Preserve collinear points that represent a direction reversal (U-turn)
|
||||
// within a single segment. E.g. (10,10)→(90,10)→(20,10): the middle point
|
||||
// is collinear but the stroker needs to see the reversal.
|
||||
// Don't preserve reversals at segment boundaries — these arise from joining
|
||||
// different path segments (e.g. arc-to-arc) and are not user-intended.
|
||||
bool preserve = false;
|
||||
if (linearReversalIndices == null || linearReversalIndices.Contains(i))
|
||||
{
|
||||
Vector2 incoming = (Vector2)points[i] - lastPoint;
|
||||
Vector2 outgoing = (Vector2)points[next] - (Vector2)points[i];
|
||||
float inLen = incoming.Length();
|
||||
float outLen = outgoing.Length();
|
||||
preserve = inLen > Epsilon && outLen > Epsilon && Vector2.Dot(incoming, outgoing) < 0;
|
||||
}
|
||||
|
||||
if (!preserve)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
results.Add(
|
||||
new PointData
|
||||
{
|
||||
Point = points[i],
|
||||
Orientation = or,
|
||||
Length = Vector2.Distance(lastPoint, points[i]),
|
||||
});
|
||||
lastPoint = points[i];
|
||||
}
|
||||
|
||||
if (isClosed && removeCloseAndCollinear)
|
||||
{
|
||||
// walk back removing collinear points
|
||||
while (results.Count > 2 && results[^1].Orientation == PointOrientation.Collinear)
|
||||
{
|
||||
results.RemoveAt(results.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return [.. results];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two points are within the specified coordinate threshold of one another.
|
||||
/// </summary>
|
||||
/// <param name="source1">The first point.</param>
|
||||
/// <param name="source2">The second point.</param>
|
||||
/// <param name="threshold">The per-axis distance threshold.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> when both coordinates are within <paramref name="threshold"/>; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool Equivalent(PointF source1, PointF source2, float threshold)
|
||||
{
|
||||
Vector2 abs = Vector2.Abs(source1 - source2);
|
||||
return abs.X < threshold && abs.Y < threshold;
|
||||
}
|
||||
|
||||
private struct PointData
|
||||
{
|
||||
public PointF Point;
|
||||
public PointOrientation Orientation;
|
||||
public float Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
ImageSharp.Drawing/IntersectionRule.cs
Normal file
20
ImageSharp.Drawing/IntersectionRule.cs
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Provides options for calculating intersection points.
|
||||
/// </summary>
|
||||
public enum IntersectionRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Only odd numbered sub-regions are filled.
|
||||
/// </summary>
|
||||
EvenOdd = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Only non-zero sub-regions are filled.
|
||||
/// </summary>
|
||||
NonZero = 1
|
||||
}
|
||||
}
|
||||
17
ImageSharp.Drawing/LineCap.cs
Normal file
17
ImageSharp.Drawing/LineCap.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <inheritdoc cref="PolygonClipper.LineCap" />
|
||||
public enum LineCap
|
||||
{
|
||||
/// <inheritdoc cref="PolygonClipper.LineCap.Butt" />
|
||||
Butt,
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.LineCap.Square" />
|
||||
Square,
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.LineCap.Round" />
|
||||
Round
|
||||
}
|
||||
}
|
||||
23
ImageSharp.Drawing/LineJoin.cs
Normal file
23
ImageSharp.Drawing/LineJoin.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <inheritdoc cref="PolygonClipper.LineJoin" />
|
||||
public enum LineJoin
|
||||
{
|
||||
/// <inheritdoc cref="PolygonClipper.LineJoin.Miter" />
|
||||
Miter = 0,
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.LineJoin.MiterRevert" />
|
||||
MiterRevert = 1,
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.LineJoin.Round" />
|
||||
Round = 2,
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.LineJoin.Bevel" />
|
||||
Bevel = 3,
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.LineJoin.MiterRound" />
|
||||
MiterRound = 4
|
||||
}
|
||||
}
|
||||
43
ImageSharp.Drawing/LinearContour.cs
Normal file
43
ImageSharp.Drawing/LinearContour.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Describes a single contour within a <see cref="LinearGeometry"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A contour identifies a contiguous point run in <see cref="LinearGeometry.Points"/> and the corresponding range in
|
||||
/// the derived segment stream exposed by <see cref="LinearGeometry.GetSegments"/>.
|
||||
/// </remarks>
|
||||
public readonly struct LinearContour
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the zero-based index of the first point belonging to this contour in <see cref="LinearGeometry.Points"/>.
|
||||
/// </summary>
|
||||
public required int PointStart { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of stored points belonging to this contour.
|
||||
/// </summary>
|
||||
public required int PointCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the zero-based index of the first derived segment belonging to this contour.
|
||||
/// </summary>
|
||||
public required int SegmentStart { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of derived segments belonging to this contour.
|
||||
/// </summary>
|
||||
public required int SegmentCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the contour is closed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When <see langword="true"/>, the final derived segment for the contour joins the last stored point back to
|
||||
/// the first stored point. Closed contours do not duplicate the first point at the end of their stored point run.
|
||||
/// </remarks>
|
||||
public required bool IsClosed { get; init; }
|
||||
}
|
||||
}
|
||||
181
ImageSharp.Drawing/LinearGeometry.cs
Normal file
181
ImageSharp.Drawing/LinearGeometry.cs
Normal file
@ -0,0 +1,181 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
ImageSharp.Drawing/LinearGeometryCache.cs
Normal file
48
ImageSharp.Drawing/LinearGeometryCache.cs
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Single-entry memoization slot for a scale-baked <see cref="LinearGeometry"/> derived from an <see cref="IPath"/>.
|
||||
/// Entries are keyed on the X/Y scale so text and panning workloads — which re-render the same shapes at a fixed
|
||||
/// zoom level while their rotation/translation/perspective drift — hit a stable cached bake.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Safe for concurrent readers and writers. Publication uses <see cref="Volatile.Write{T}(ref T, T)"/> so a reader
|
||||
/// either observes <see langword="null"/> or a fully-constructed entry.
|
||||
/// </remarks>
|
||||
internal struct LinearGeometryCache
|
||||
{
|
||||
private Entry? entry;
|
||||
|
||||
public bool TryGet(Vector2 scale, [NotNullWhen(true)] out LinearGeometry? value)
|
||||
{
|
||||
Entry? hit = Volatile.Read(ref this.entry);
|
||||
if (hit is not null && hit.Scale == scale)
|
||||
{
|
||||
value = hit.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public LinearGeometry Store(Vector2 scale, LinearGeometry value)
|
||||
{
|
||||
Volatile.Write(ref this.entry, new Entry(scale, value));
|
||||
return value;
|
||||
}
|
||||
|
||||
private sealed class Entry(Vector2 scale, LinearGeometry value)
|
||||
{
|
||||
public Vector2 Scale { get; } = scale;
|
||||
|
||||
public LinearGeometry Value { get; } = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
ImageSharp.Drawing/LinearGeometryInfo.cs
Normal file
52
ImageSharp.Drawing/LinearGeometryInfo.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Describes geometry-wide metadata for a <see cref="LinearGeometry"/> instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This metadata is computed eagerly during lowering so backends do not need to enumerate the geometry again to
|
||||
/// discover basic information such as total segment count or bounds.
|
||||
/// </remarks>
|
||||
public readonly struct LinearGeometryInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the bounds of all points stored in the containing <see cref="LinearGeometry"/>.
|
||||
/// </summary>
|
||||
public required RectangleF Bounds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of contours in the containing <see cref="LinearGeometry"/>.
|
||||
/// </summary>
|
||||
public required int ContourCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of stored points across all contours.
|
||||
/// </summary>
|
||||
public required int PointCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of derived linear segments across all contours.
|
||||
/// </summary>
|
||||
public required int SegmentCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of derived segments that remain non-horizontal when sampled on pixel boundaries.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A segment contributes to this count when its start and end sample into different rows under
|
||||
/// pixel-boundary sampling.
|
||||
/// </remarks>
|
||||
public required int NonHorizontalSegmentCountPixelBoundary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of derived segments that remain non-horizontal when sampled at pixel centers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A segment contributes to this count when its start and end sample into different rows after the
|
||||
/// half-pixel center-sampling offset is applied.
|
||||
/// </remarks>
|
||||
public required int NonHorizontalSegmentCountPixelCenter { get; init; }
|
||||
}
|
||||
}
|
||||
144
ImageSharp.Drawing/LinearLineSegment.cs
Normal file
144
ImageSharp.Drawing/LinearLineSegment.cs
Normal file
@ -0,0 +1,144 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Drawing.Helpers;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents a series of control points that will be joined by straight lines
|
||||
/// </summary>
|
||||
/// <seealso cref="ILineSegment" />
|
||||
public sealed class LinearLineSegment : ILineSegment
|
||||
{
|
||||
/// <summary>
|
||||
/// The collection of points.
|
||||
/// </summary>
|
||||
private readonly PointF[] points;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearLineSegment"/> class.
|
||||
/// </summary>
|
||||
/// <param name="start">The start.</param>
|
||||
/// <param name="end">The end.</param>
|
||||
public LinearLineSegment(PointF start, PointF end)
|
||||
: this([start, end])
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearLineSegment" /> class.
|
||||
/// </summary>
|
||||
/// <param name="point1">The point1.</param>
|
||||
/// <param name="point2">The point2.</param>
|
||||
/// <param name="additionalPoints">Additional points</param>
|
||||
public LinearLineSegment(PointF point1, PointF point2, params PointF[] additionalPoints)
|
||||
: this(new[] { point1, point2 }.Concat(additionalPoints))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearLineSegment"/> class.
|
||||
/// </summary>
|
||||
/// <param name="points">The points.</param>
|
||||
public LinearLineSegment(PointF[] points)
|
||||
{
|
||||
Guard.NotNull(points, nameof(points));
|
||||
Guard.MustBeGreaterThanOrEqualTo(points.Length, 2, nameof(points));
|
||||
this.points = points;
|
||||
this.Bounds = CalculateBounds(points);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start point.
|
||||
/// </summary>
|
||||
public PointF StartPoint => this.points[0];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The end point.
|
||||
/// </value>
|
||||
public PointF EndPoint => this.points[^1];
|
||||
|
||||
/// <inheritdoc />
|
||||
public RectangleF Bounds { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int LinearVertexCount(Vector2 scale) => this.points.Length;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CopyTo(Span<PointF> destination, bool skipFirstPoint, Vector2 scale)
|
||||
{
|
||||
int startIndex = skipFirstPoint ? 1 : 0;
|
||||
ReadOnlySpan<PointF> source = this.points.AsSpan(startIndex);
|
||||
|
||||
if (scale == Vector2.One)
|
||||
{
|
||||
source.CopyTo(destination);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
destination[i] = new PointF(source[i].X * scale.X, source[i].Y * scale.Y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the current LineSegment using specified matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The matrix.</param>
|
||||
/// <returns>
|
||||
/// A line segment with the matrix applied to it.
|
||||
/// </returns>
|
||||
public LinearLineSegment Transform(Matrix4x4 matrix)
|
||||
{
|
||||
if (matrix.IsIdentity)
|
||||
{
|
||||
// no transform to apply skip it
|
||||
return this;
|
||||
}
|
||||
|
||||
PointF[] transformedPoints = new PointF[this.points.Length];
|
||||
|
||||
for (int i = 0; i < this.points.Length; i++)
|
||||
{
|
||||
transformedPoints[i] = PointF.Transform(this.points[i], matrix);
|
||||
}
|
||||
|
||||
return new LinearLineSegment(transformedPoints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the current LineSegment using specified matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The matrix.</param>
|
||||
/// <returns>A line segment with the matrix applied to it.</returns>
|
||||
ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the bounds for the retained linear point run.
|
||||
/// </summary>
|
||||
private static RectangleF CalculateBounds(ReadOnlySpan<PointF> points)
|
||||
{
|
||||
float minX = float.MaxValue;
|
||||
float minY = float.MaxValue;
|
||||
float maxX = float.MinValue;
|
||||
float maxY = float.MinValue;
|
||||
|
||||
for (int i = 0; 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
ImageSharp.Drawing/LinearSegment.cs
Normal file
43
ImageSharp.Drawing/LinearSegment.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents one derived linear segment within a <see cref="LinearGeometry"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Instances are produced by <see cref="SegmentEnumerator"/> and contain the per-segment values required by current
|
||||
/// backend scene-building code without forcing each backend to recompute them from the endpoints on every iteration.
|
||||
/// </remarks>
|
||||
public readonly struct LinearSegment
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the segment start point.
|
||||
/// </summary>
|
||||
public required PointF Start { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment end point.
|
||||
/// </summary>
|
||||
public required PointF End { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the smaller of <see cref="Start"/>.<see cref="PointF.Y"/> and <see cref="End"/>.<see cref="PointF.Y"/>.
|
||||
/// </summary>
|
||||
public required float MinY { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the larger of <see cref="Start"/>.<see cref="PointF.Y"/> and <see cref="End"/>.<see cref="PointF.Y"/>.
|
||||
/// </summary>
|
||||
public required float MaxY { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the segment is horizontal.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A segment is horizontal when <see cref="Start"/>.<see cref="PointF.Y"/> equals
|
||||
/// <see cref="End"/>.<see cref="PointF.Y"/>.
|
||||
/// </remarks>
|
||||
public required bool IsHorizontal { get; init; }
|
||||
}
|
||||
}
|
||||
119
ImageSharp.Drawing/OutlinePathExtensions.cs
Normal file
119
ImageSharp.Drawing/OutlinePathExtensions.cs
Normal file
@ -0,0 +1,119 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Drawing.PolygonGeometry;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Extensions to <see cref="IPath"/> that allow the generation of outlines.
|
||||
/// </summary>
|
||||
public static class OutlinePathExtensions
|
||||
{
|
||||
private static readonly StrokeOptions DefaultOptions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Generates an outline of the path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to outline</param>
|
||||
/// <param name="width">The outline width.</param>
|
||||
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
|
||||
public static IPath GenerateOutline(this IPath path, float width)
|
||||
=> GenerateOutline(path, width, DefaultOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an outline of the path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to outline</param>
|
||||
/// <param name="width">The outline width.</param>
|
||||
/// <param name="strokeOptions">The stroke geometry options.</param>
|
||||
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
|
||||
public static IPath GenerateOutline(this IPath path, float width, StrokeOptions strokeOptions)
|
||||
{
|
||||
if (width <= 0)
|
||||
{
|
||||
return Path.Empty;
|
||||
}
|
||||
|
||||
return StrokedShapeGenerator.GenerateStrokedShapes(path, width, strokeOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an outline of the path with alternating on and off segments based on the pattern.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to outline</param>
|
||||
/// <param name="width">The outline width.</param>
|
||||
/// <param name="pattern">The pattern made of multiples of the width.</param>
|
||||
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
|
||||
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern)
|
||||
=> path.GenerateOutline(width, pattern, false);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an outline of the path with alternating on and off segments based on the pattern.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to outline</param>
|
||||
/// <param name="width">The outline width.</param>
|
||||
/// <param name="pattern">The pattern made of multiples of the width.</param>
|
||||
/// <param name="strokeOptions">The stroke geometry options.</param>
|
||||
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
|
||||
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, StrokeOptions strokeOptions)
|
||||
=> GenerateOutline(path, width, pattern, false, strokeOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an outline of the path with alternating on and off segments based on the pattern.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to outline</param>
|
||||
/// <param name="width">The outline width.</param>
|
||||
/// <param name="pattern">The pattern made of multiples of the width.</param>
|
||||
/// <param name="startOff">Whether the first item in the pattern is on or off.</param>
|
||||
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
|
||||
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, bool startOff)
|
||||
=> GenerateOutline(path, width, pattern, startOff, DefaultOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an outline of the path with alternating on and off segments based on the pattern.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to outline</param>
|
||||
/// <param name="width">The outline width.</param>
|
||||
/// <param name="pattern">The pattern made of multiples of the width.</param>
|
||||
/// <param name="startOff">Whether the first item in the pattern is on or off.</param>
|
||||
/// <param name="strokeOptions">The stroke geometry options.</param>
|
||||
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
|
||||
public static IPath GenerateOutline(
|
||||
this IPath path,
|
||||
float width,
|
||||
ReadOnlySpan<float> pattern,
|
||||
bool startOff,
|
||||
StrokeOptions strokeOptions)
|
||||
{
|
||||
if (width <= 0)
|
||||
{
|
||||
return Path.Empty;
|
||||
}
|
||||
|
||||
if (pattern.Length < 2)
|
||||
{
|
||||
return path.GenerateOutline(width, strokeOptions);
|
||||
}
|
||||
|
||||
IPath dashed = path.GenerateDashes(width, pattern, startOff);
|
||||
|
||||
// GenerateDashes returns the original path when the pattern is degenerate
|
||||
// or when segmentation would exceed safety limits; stroke it as solid.
|
||||
if (ReferenceEquals(dashed, path))
|
||||
{
|
||||
return path.GenerateOutline(width, strokeOptions);
|
||||
}
|
||||
|
||||
if (dashed == Path.Empty)
|
||||
{
|
||||
return Path.Empty;
|
||||
}
|
||||
|
||||
// Each dash segment is an open sub-path; stroke expansion and boolean merge
|
||||
// are handled by the generator.
|
||||
return StrokedShapeGenerator.GenerateStrokedShapes(dashed, width, strokeOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
718
ImageSharp.Drawing/Path.cs
Normal file
718
ImageSharp.Drawing/Path.cs
Normal file
@ -0,0 +1,718 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// A aggregate of <see cref="ILineSegment"/>s making a single logical path.
|
||||
/// </summary>
|
||||
/// <seealso cref="IPath" />
|
||||
public class Path : IPath, ISimplePath, IPathInternals, IInternalPathOwner
|
||||
{
|
||||
private readonly ILineSegment[] lineSegments;
|
||||
private InternalPath? innerPath;
|
||||
private IReadOnlyList<InternalPath>? internalPathRings;
|
||||
private IPath? closedPath;
|
||||
private LinearGeometryCache geometryCache;
|
||||
private RectangleF? bounds;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Path"/> class.
|
||||
/// </summary>
|
||||
/// <param name="points">The collection of points; processed as a series of linear line segments.</param>
|
||||
public Path(PointF[] points)
|
||||
: this(new LinearLineSegment(points))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Path"/> class.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments.</param>
|
||||
public Path(IEnumerable<ILineSegment> segments)
|
||||
: this(GetSegmentArray(segments))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Path" /> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
public Path(Path path)
|
||||
: this(path.LineSegments)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Path"/> class.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments.</param>
|
||||
public Path(params ILineSegment[] segments)
|
||||
{
|
||||
Guard.NotNull(segments, nameof(segments));
|
||||
this.lineSegments = segments;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default empty path.
|
||||
/// </summary>
|
||||
public static IPath Empty { get; } = EmptyPath.OpenPath;
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool ISimplePath.IsClosed => this.IsClosed;
|
||||
|
||||
/// <inheritdoc cref="ISimplePath.IsClosed"/>
|
||||
public virtual bool IsClosed => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ReadOnlyMemory<PointF> Points => this.InnerPath.Points();
|
||||
|
||||
/// <inheritdoc />
|
||||
public RectangleF Bounds => this.bounds ??= this.CalculateBounds();
|
||||
|
||||
/// <inheritdoc />
|
||||
public PathTypes PathType => this.IsClosed ? PathTypes.Closed : PathTypes.Open;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum number intersections that a shape can have when testing a line.
|
||||
/// </summary>
|
||||
internal int MaxIntersections => this.InnerPath.PointCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets readonly collection of line segments.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ILineSegment> LineSegments => this.lineSegments;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether close or collinear vertices should be removed. TEST ONLY!
|
||||
/// </summary>
|
||||
internal bool RemoveCloseAndCollinearPoints { get; set; } = true;
|
||||
|
||||
private protected InternalPath InnerPath =>
|
||||
this.innerPath ??= new InternalPath(this.lineSegments, this.IsClosed, this.RemoveCloseAndCollinearPoints);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IPath Transform(Matrix4x4 matrix)
|
||||
{
|
||||
if (matrix.IsIdentity)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
ILineSegment[] segments = new ILineSegment[this.lineSegments.Length];
|
||||
|
||||
for (int i = 0; i < segments.Length; i++)
|
||||
{
|
||||
segments[i] = this.lineSegments[i].Transform(matrix);
|
||||
}
|
||||
|
||||
return new Path(segments);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPath AsClosedPath()
|
||||
{
|
||||
if (this.IsClosed)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
return this.closedPath ??= new Polygon(this.LineSegments);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ISimplePath> Flatten()
|
||||
{
|
||||
yield return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual LinearGeometry ToLinearGeometry(Vector2 scale)
|
||||
=> this.geometryCache.TryGet(scale, out LinearGeometry? hit)
|
||||
? hit
|
||||
: this.geometryCache.Store(scale, this.BuildLinearGeometry(scale));
|
||||
|
||||
private LinearGeometry BuildLinearGeometry(Vector2 scale)
|
||||
{
|
||||
if (this.lineSegments.Length == 0)
|
||||
{
|
||||
return new LinearGeometry(
|
||||
new LinearGeometryInfo
|
||||
{
|
||||
Bounds = RectangleF.Empty,
|
||||
ContourCount = 0,
|
||||
PointCount = 0,
|
||||
SegmentCount = 0,
|
||||
NonHorizontalSegmentCountPixelBoundary = 0,
|
||||
NonHorizontalSegmentCountPixelCenter = 0
|
||||
},
|
||||
[],
|
||||
[]);
|
||||
}
|
||||
|
||||
PointF? lastEndPoint = null;
|
||||
int pointCount = 0;
|
||||
|
||||
for (int i = 0; i < this.lineSegments.Length; i++)
|
||||
{
|
||||
ILineSegment segment = this.lineSegments[i];
|
||||
bool skipFirstPoint = lastEndPoint?.Equals(segment.StartPoint) == true;
|
||||
pointCount += segment.LinearVertexCount(scale) - (skipFirstPoint ? 1 : 0);
|
||||
lastEndPoint = segment.EndPoint;
|
||||
}
|
||||
|
||||
PointF[] points = new PointF[pointCount];
|
||||
LinearContour[] contours = pointCount == 0 ? [] : new LinearContour[1];
|
||||
|
||||
bool hasBounds = false;
|
||||
float minX = float.MaxValue;
|
||||
float minY = float.MaxValue;
|
||||
float maxX = float.MinValue;
|
||||
float maxY = float.MinValue;
|
||||
int nonHorizontalSegmentCountPixelBoundary = 0;
|
||||
int nonHorizontalSegmentCountPixelCenter = 0;
|
||||
int pointIndex = 0;
|
||||
lastEndPoint = null;
|
||||
|
||||
for (int i = 0; i < this.lineSegments.Length; i++)
|
||||
{
|
||||
ILineSegment segment = this.lineSegments[i];
|
||||
bool skipFirstPoint = lastEndPoint?.Equals(segment.StartPoint) == true;
|
||||
int contributionCount = segment.LinearVertexCount(scale) - (skipFirstPoint ? 1 : 0);
|
||||
Span<PointF> destination = points.AsSpan(pointIndex, contributionCount);
|
||||
|
||||
segment.CopyTo(destination, skipFirstPoint, scale);
|
||||
lastEndPoint = segment.EndPoint;
|
||||
|
||||
for (int p = 0; p < destination.Length; p++)
|
||||
{
|
||||
PointF point = destination[p];
|
||||
minX = MathF.Min(minX, point.X);
|
||||
minY = MathF.Min(minY, point.Y);
|
||||
maxX = MathF.Max(maxX, point.X);
|
||||
maxY = MathF.Max(maxY, point.Y);
|
||||
hasBounds = true;
|
||||
}
|
||||
|
||||
pointIndex += contributionCount;
|
||||
}
|
||||
|
||||
int segmentCount = pointCount == 0 ? 0 : this.IsClosed ? pointCount : pointCount - 1;
|
||||
CountNonHorizontalSegments(points, pointCount, this.IsClosed, ref nonHorizontalSegmentCountPixelBoundary, ref nonHorizontalSegmentCountPixelCenter);
|
||||
|
||||
if (pointCount > 0)
|
||||
{
|
||||
contours[0] = new LinearContour
|
||||
{
|
||||
PointStart = 0,
|
||||
PointCount = pointCount,
|
||||
SegmentStart = 0,
|
||||
SegmentCount = segmentCount,
|
||||
IsClosed = this.IsClosed
|
||||
};
|
||||
}
|
||||
|
||||
RectangleF bounds = hasBounds ? RectangleF.FromLTRB(minX, minY, maxX, maxY) : RectangleF.Empty;
|
||||
|
||||
return new LinearGeometry(
|
||||
new LinearGeometryInfo
|
||||
{
|
||||
Bounds = bounds,
|
||||
ContourCount = contours.Length,
|
||||
PointCount = points.Length,
|
||||
SegmentCount = segmentCount,
|
||||
NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary,
|
||||
NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter
|
||||
},
|
||||
contours,
|
||||
points);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
SegmentInfo IPathInternals.PointAlongPath(float distance)
|
||||
=> this.InnerPath.PointAlongPath(distance);
|
||||
|
||||
/// <inheritdoc/>
|
||||
IReadOnlyList<InternalPath> IInternalPathOwner.GetRingsAsInternalPath()
|
||||
=> this.internalPathRings ??= [this.InnerPath];
|
||||
|
||||
/// <summary>
|
||||
/// Computes path bounds directly from segment bounds without materializing <see cref="InternalPath"/>.
|
||||
/// </summary>
|
||||
private RectangleF CalculateBounds()
|
||||
{
|
||||
if (this.lineSegments.Length == 0)
|
||||
{
|
||||
return RectangleF.Empty;
|
||||
}
|
||||
|
||||
RectangleF bounds = this.lineSegments[0].Bounds;
|
||||
|
||||
for (int i = 1; i < this.lineSegments.Length; i++)
|
||||
{
|
||||
bounds = RectangleF.Union(bounds, this.lineSegments[i].Bounds);
|
||||
}
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Materializes the segment sequence into the retained array used by the path.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segment sequence to materialize.</param>
|
||||
/// <returns>The retained segment array.</returns>
|
||||
private static ILineSegment[] GetSegmentArray(IEnumerable<ILineSegment> segments)
|
||||
{
|
||||
Guard.NotNull(segments, nameof(segments));
|
||||
return segments as ILineSegment[] ?? [.. segments];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts how many derived segments survive as non-horizontal raster work for each sampling origin.
|
||||
/// </summary>
|
||||
/// <param name="points">The retained contour point run.</param>
|
||||
/// <param name="pointCount">The number of retained points in the contour.</param>
|
||||
/// <param name="isClosed">Whether the contour closes back to its first point.</param>
|
||||
/// <param name="nonHorizontalSegmentCountPixelBoundary">The accumulated pixel-boundary count to update.</param>
|
||||
/// <param name="nonHorizontalSegmentCountPixelCenter">The accumulated pixel-center count to update.</param>
|
||||
private static void CountNonHorizontalSegments(
|
||||
ReadOnlySpan<PointF> points,
|
||||
int pointCount,
|
||||
bool isClosed,
|
||||
ref int nonHorizontalSegmentCountPixelBoundary,
|
||||
ref int nonHorizontalSegmentCountPixelCenter)
|
||||
{
|
||||
if (pointCount <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int segmentCount = isClosed ? pointCount : pointCount - 1;
|
||||
for (int i = 0; i < segmentCount; i++)
|
||||
{
|
||||
PointF start = points[i];
|
||||
PointF end = points[(i + 1) == pointCount ? 0 : i + 1];
|
||||
if (ToFixedBoundary(start.Y) != ToFixedBoundary(end.Y))
|
||||
{
|
||||
nonHorizontalSegmentCountPixelBoundary++;
|
||||
}
|
||||
|
||||
if (ToFixedCenter(start.Y) != ToFixedCenter(end.Y))
|
||||
{
|
||||
nonHorizontalSegmentCountPixelCenter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a coordinate to the fixed-point row space used by boundary-sampled raster work.
|
||||
/// </summary>
|
||||
/// <param name="value">The coordinate to convert.</param>
|
||||
/// <returns>The rounded 24.8 fixed-point value.</returns>
|
||||
private static int ToFixedBoundary(float value) => (int)MathF.Round(value * 256F);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a coordinate to the fixed-point row space used by center-sampled raster work.
|
||||
/// </summary>
|
||||
/// <param name="value">The coordinate to convert.</param>
|
||||
/// <returns>The rounded 24.8 fixed-point value after the half-pixel sampling offset is applied.</returns>
|
||||
private static int ToFixedCenter(float value) => (int)MathF.Round((value + 0.5F) * 256F);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an SVG path string into an <see cref="IPath"/>.
|
||||
/// </summary>
|
||||
/// <param name="svgPath">The string containing the SVG path data.</param>
|
||||
/// <param name="value">
|
||||
/// When this method returns, contains the logic path converted from the given SVG path string; otherwise, <see langword="null"/>.
|
||||
/// This parameter is passed uninitialized.
|
||||
/// </param>
|
||||
/// <returns><see langword="true"/> if the input value can be parsed and converted; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool TryParseSvgPath(string svgPath, [NotNullWhen(true)] out IPath? value)
|
||||
=> TryParseSvgPath(svgPath.AsSpan(), out value);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an SVG path string into an <see cref="IPath"/>.
|
||||
/// </summary>
|
||||
/// <param name="svgPath">The string containing the SVG path data.</param>
|
||||
/// <param name="value">
|
||||
/// When this method returns, contains the logic path converted from the given SVG path string; otherwise, <see langword="null"/>.
|
||||
/// This parameter is passed uninitialized.
|
||||
/// </param>
|
||||
/// <returns><see langword="true"/> if the input value can be parsed and converted; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool TryParseSvgPath(ReadOnlySpan<char> svgPath, [NotNullWhen(true)] out IPath? value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
PathBuilder builder = new();
|
||||
|
||||
PointF first = PointF.Empty;
|
||||
PointF c = PointF.Empty;
|
||||
PointF lastc = PointF.Empty;
|
||||
PointF point1;
|
||||
PointF point2;
|
||||
PointF point3;
|
||||
|
||||
char op = '\0';
|
||||
char previousOp = '\0';
|
||||
bool relative = false;
|
||||
while (true)
|
||||
{
|
||||
svgPath = svgPath.TrimStart();
|
||||
if (svgPath.Length == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
char ch = svgPath[0];
|
||||
if (char.IsDigit(ch) || ch == '-' || ch == '+' || ch == '.')
|
||||
{
|
||||
// SVG allows repeated operand groups to reuse the previous command.
|
||||
// A leading number is only valid once a drawable command is active.
|
||||
if (op is '\0' or 'Z')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (IsSeparator(ch))
|
||||
{
|
||||
svgPath = TrimSeparator(svgPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
op = ch;
|
||||
relative = false;
|
||||
if (char.IsLower(op))
|
||||
{
|
||||
op = char.ToUpper(op, CultureInfo.InvariantCulture);
|
||||
relative = true;
|
||||
}
|
||||
|
||||
svgPath = TrimSeparator(svgPath[1..]);
|
||||
}
|
||||
|
||||
// Read every operand for the command before appending geometry. That keeps
|
||||
// malformed or truncated data from leaking a partially parsed segment into the path.
|
||||
switch (op)
|
||||
{
|
||||
case 'M':
|
||||
if (!TryFindPoint(ref svgPath, relative, c, out point1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = builder.MoveTo(point1);
|
||||
previousOp = '\0';
|
||||
|
||||
// Extra coordinate pairs after a move command are implicit line commands.
|
||||
op = 'L';
|
||||
c = point1;
|
||||
break;
|
||||
case 'L':
|
||||
if (!TryFindPoint(ref svgPath, relative, c, out point1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = builder.LineTo(point1);
|
||||
c = point1;
|
||||
break;
|
||||
case 'H':
|
||||
if (!TryFindScaler(ref svgPath, out float x))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (relative)
|
||||
{
|
||||
x += c.X;
|
||||
}
|
||||
|
||||
if (!float.IsFinite(x))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = builder.LineTo(x, c.Y);
|
||||
c.X = x;
|
||||
break;
|
||||
case 'V':
|
||||
if (!TryFindScaler(ref svgPath, out float y))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (relative)
|
||||
{
|
||||
y += c.Y;
|
||||
}
|
||||
|
||||
if (!float.IsFinite(y))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = builder.LineTo(c.X, y);
|
||||
c.Y = y;
|
||||
break;
|
||||
case 'C':
|
||||
if (!TryFindPoint(ref svgPath, relative, c, out point1)
|
||||
|| !TryFindPoint(ref svgPath, relative, c, out point2)
|
||||
|| !TryFindPoint(ref svgPath, relative, c, out point3))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = builder.CubicBezierTo(point1, point2, point3);
|
||||
lastc = point2;
|
||||
c = point3;
|
||||
break;
|
||||
case 'S':
|
||||
if (!TryFindPoint(ref svgPath, relative, c, out point2)
|
||||
|| !TryFindPoint(ref svgPath, relative, c, out point3))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
point1 = c;
|
||||
if (previousOp is 'C' or 'S')
|
||||
{
|
||||
// Smooth cubic curves mirror the previous cubic control point.
|
||||
// Without a preceding cubic command, the current point is the control point.
|
||||
point1.X -= lastc.X - c.X;
|
||||
point1.Y -= lastc.Y - c.Y;
|
||||
}
|
||||
|
||||
_ = builder.CubicBezierTo(point1, point2, point3);
|
||||
lastc = point2;
|
||||
c = point3;
|
||||
break;
|
||||
case 'Q': // Quadratic Bezier Curve
|
||||
if (!TryFindPoint(ref svgPath, relative, c, out point1)
|
||||
|| !TryFindPoint(ref svgPath, relative, c, out point2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = builder.QuadraticBezierTo(point1, point2);
|
||||
lastc = point1;
|
||||
c = point2;
|
||||
break;
|
||||
case 'T':
|
||||
if (!TryFindPoint(ref svgPath, relative, c, out point2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
point1 = c;
|
||||
if (previousOp is 'Q' or 'T')
|
||||
{
|
||||
// Smooth quadratic curves mirror the previous quadratic control point.
|
||||
// Without a preceding quadratic command, the current point is the control point.
|
||||
point1.X -= lastc.X - c.X;
|
||||
point1.Y -= lastc.Y - c.Y;
|
||||
}
|
||||
|
||||
_ = builder.QuadraticBezierTo(point1, point2);
|
||||
lastc = point1;
|
||||
c = point2;
|
||||
break;
|
||||
case 'A':
|
||||
// Arc flags are single SVG grammar tokens, not numbers. Reading them as
|
||||
// scalars would accept malformed flag/end-point boundaries such as "04445".
|
||||
if (!TryFindScaler(ref svgPath, out float radiiX)
|
||||
|| !TryTrimSeparator(ref svgPath)
|
||||
|| !TryFindScaler(ref svgPath, out float radiiY)
|
||||
|| !TryTrimSeparator(ref svgPath)
|
||||
|| !TryFindScaler(ref svgPath, out float angle)
|
||||
|| !TryTrimSeparator(ref svgPath)
|
||||
|| !TryFindFlag(ref svgPath, out bool largeArc)
|
||||
|| !TryTrimSeparator(ref svgPath)
|
||||
|| !TryFindFlag(ref svgPath, out bool sweep)
|
||||
|| !TryFindPoint(ref svgPath, relative, c, out PointF point))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = builder.ArcTo(radiiX, radiiY, angle, largeArc, sweep, point);
|
||||
c = point;
|
||||
break;
|
||||
case 'Z':
|
||||
_ = builder.CloseFigure();
|
||||
c = first;
|
||||
break;
|
||||
case '~':
|
||||
if (!TryFindPoint(ref svgPath, relative, c, out point1)
|
||||
|| !TryFindPoint(ref svgPath, relative, c, out point2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = builder.MoveTo(point1).LineTo(point2);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (previousOp == 0)
|
||||
{
|
||||
first = c;
|
||||
}
|
||||
|
||||
previousOp = op;
|
||||
}
|
||||
|
||||
value = builder.Build();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryFindFlag(ref ReadOnlySpan<char> str, out bool value)
|
||||
{
|
||||
str = TrimSeparator(str);
|
||||
|
||||
// https://www.w3.org/TR/SVG11/paths.html#PathDataBNF
|
||||
// flag: "0" | "1"
|
||||
// Adjacent flags are valid, so this consumes exactly one character.
|
||||
if (str.Length == 0 || (str[0] is not '0' and not '1'))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = str[0] == '1';
|
||||
str = str[1..];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryTrimSeparator(ref ReadOnlySpan<char> str)
|
||||
{
|
||||
// SVG separators are optional in places where the next token can be
|
||||
// recognized unambiguously. Keep this chainable with the operand readers.
|
||||
ReadOnlySpan<char> result = TrimSeparator(str);
|
||||
if (str[^result.Length..].StartsWith(result))
|
||||
{
|
||||
str = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFindScaler(ref ReadOnlySpan<char> str, out float value)
|
||||
{
|
||||
ReadOnlySpan<char> source = TrimSeparator(str);
|
||||
if (TryReadScalar(source, out value, out int length))
|
||||
{
|
||||
str = source[length..];
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFindPoint(ref ReadOnlySpan<char> str, bool relative, PointF current, out PointF value)
|
||||
{
|
||||
if (TryFindScaler(ref str, out float x) && TryFindScaler(ref str, out float y))
|
||||
{
|
||||
// Relative operands can overflow after adding the current point even when
|
||||
// each parsed scalar is finite, so validate the absolute result as well.
|
||||
if (relative)
|
||||
{
|
||||
x += current.X;
|
||||
y += current.Y;
|
||||
}
|
||||
|
||||
if (!float.IsFinite(x) || !float.IsFinite(y))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = new PointF(x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryReadScalar(ReadOnlySpan<char> str, out float scaler, out int length)
|
||||
{
|
||||
// SVG path numbers can be tightly packed: "10-20" is two numbers, as is
|
||||
// "0.5.6". Stop at the first character that belongs to the next token.
|
||||
bool hasDot = false;
|
||||
for (int i = 0; i < str.Length; i++)
|
||||
{
|
||||
char ch = str[i];
|
||||
|
||||
if (IsSeparator(ch))
|
||||
{
|
||||
length = i;
|
||||
return TryParseFloat(str[..length], out scaler);
|
||||
}
|
||||
|
||||
if (ch == '.')
|
||||
{
|
||||
if (hasDot)
|
||||
{
|
||||
// Second decimal point starts a new number.
|
||||
length = i;
|
||||
return TryParseFloat(str[..length], out scaler);
|
||||
}
|
||||
|
||||
hasDot = true;
|
||||
}
|
||||
else if ((ch is '-' or '+') && i > 0)
|
||||
{
|
||||
// A sign character mid-number starts a new number,
|
||||
// unless it follows an exponent indicator.
|
||||
char prev = str[i - 1];
|
||||
if (prev is not 'e' and not 'E')
|
||||
{
|
||||
length = i;
|
||||
return TryParseFloat(str[..length], out scaler);
|
||||
}
|
||||
}
|
||||
else if (char.IsLetter(ch))
|
||||
{
|
||||
// Hit a command letter; end this number.
|
||||
length = i;
|
||||
return TryParseFloat(str[..length], out scaler);
|
||||
}
|
||||
}
|
||||
|
||||
length = str.Length;
|
||||
return TryParseFloat(str, out scaler);
|
||||
}
|
||||
|
||||
private static bool IsSeparator(char ch)
|
||||
=> char.IsWhiteSpace(ch) || ch == ',';
|
||||
|
||||
private static ReadOnlySpan<char> TrimSeparator(ReadOnlySpan<char> data)
|
||||
{
|
||||
if (data.Length == 0)
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
for (; idx < data.Length; idx++)
|
||||
{
|
||||
if (!IsSeparator(data[idx]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return data[idx..];
|
||||
}
|
||||
|
||||
private static bool TryParseFloat(ReadOnlySpan<char> str, out float value)
|
||||
=> float.TryParse(str, CultureInfo.InvariantCulture, out value) && float.IsFinite(value);
|
||||
}
|
||||
}
|
||||
787
ImageSharp.Drawing/PathBuilder.cs
Normal file
787
ImageSharp.Drawing/PathBuilder.cs
Normal file
@ -0,0 +1,787 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Allow you to derivatively build shapes and paths.
|
||||
/// </summary>
|
||||
public class PathBuilder
|
||||
{
|
||||
private readonly List<Figure> figures = [];
|
||||
private readonly Matrix4x4 defaultTransform;
|
||||
private Figure currentFigure;
|
||||
private Matrix4x4 currentTransform;
|
||||
private Matrix4x4 setTransform;
|
||||
private Vector2 currentPoint;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathBuilder" /> class.
|
||||
/// </summary>
|
||||
public PathBuilder()
|
||||
: this(Matrix4x4.Identity)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathBuilder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="defaultTransform">The default transform.</param>
|
||||
public PathBuilder(Matrix4x4 defaultTransform)
|
||||
{
|
||||
this.defaultTransform = defaultTransform;
|
||||
this.Clear();
|
||||
_ = this.ResetTransform();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current transformation matrix.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns a copy of the matrix. Because <see cref="Matrix4x4"/> is a value type,
|
||||
/// modifications to the returned value do not affect the internal state. To change the transform,
|
||||
/// call <see cref="SetTransform(Matrix4x4)"/>.
|
||||
/// </remarks>
|
||||
/// <value>The current transformation matrix.</value>
|
||||
public Matrix4x4 Transform => this.currentTransform;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the translation to be applied to all items to follow being applied to the <see cref="PathBuilder"/>.
|
||||
/// </summary>
|
||||
/// <param name="transform">The transform.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder SetTransform(Matrix4x4 transform)
|
||||
{
|
||||
this.setTransform = transform;
|
||||
this.currentTransform = this.setTransform * this.defaultTransform;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the origin all subsequent point should be relative to.
|
||||
/// </summary>
|
||||
/// <param name="origin">The origin.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder SetOrigin(PointF origin)
|
||||
{
|
||||
// The new origin should be transformed based on the default transform
|
||||
this.setTransform.Translation = new Vector3(origin.X, origin.Y, 0);
|
||||
this.currentTransform = this.setTransform * this.defaultTransform;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the transform to the default.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder ResetTransform()
|
||||
{
|
||||
this.setTransform = Matrix4x4.Identity;
|
||||
this.currentTransform = this.setTransform * this.defaultTransform;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the origin to the default.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder ResetOrigin()
|
||||
{
|
||||
this.setTransform.Translation = Vector3.Zero;
|
||||
this.currentTransform = this.setTransform * this.defaultTransform;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves to current point to the supplied vector.
|
||||
/// </summary>
|
||||
/// <param name="point">The point.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder MoveTo(PointF point)
|
||||
{
|
||||
_ = this.StartFigure();
|
||||
this.currentPoint = PointF.Transform(point, this.currentTransform);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves to current point to the supplied vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate.</param>
|
||||
/// <param name="y">The y-coordinate.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/></returns>
|
||||
public PathBuilder MoveTo(float x, float y)
|
||||
=> this.MoveTo(new PointF(x, y));
|
||||
|
||||
/// <summary>
|
||||
/// Draws the line connecting the current the current point to the new point.
|
||||
/// </summary>
|
||||
/// <param name="point">The point.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder LineTo(PointF point)
|
||||
=> this.AddLine(this.currentPoint, point);
|
||||
|
||||
/// <summary>
|
||||
/// Draws the line connecting the current the current point to the new point.
|
||||
/// </summary>
|
||||
/// <param name="x">The x.</param>
|
||||
/// <param name="y">The y.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/></returns>
|
||||
public PathBuilder LineTo(float x, float y)
|
||||
=> this.LineTo(new PointF(x, y));
|
||||
|
||||
/// <summary>
|
||||
/// Adds the line connecting the current point to the new point.
|
||||
/// </summary>
|
||||
/// <param name="start">The start.</param>
|
||||
/// <param name="end">The end.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddLine(PointF start, PointF end)
|
||||
=> this.AddSegment(new LinearLineSegment(start, end));
|
||||
|
||||
/// <summary>
|
||||
/// Adds the line connecting the current point to the new point.
|
||||
/// </summary>
|
||||
/// <param name="x1">The x1.</param>
|
||||
/// <param name="y1">The y1.</param>
|
||||
/// <param name="x2">The x2.</param>
|
||||
/// <param name="y2">The y2.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddLine(float x1, float y1, float x2, float y2)
|
||||
=> this.AddLine(new PointF(x1, y1), new PointF(x2, y2));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a series of line segments connecting the current point to the new points.
|
||||
/// </summary>
|
||||
/// <param name="points">The points.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddLines(IEnumerable<PointF> points)
|
||||
{
|
||||
Guard.NotNull(points, nameof(points));
|
||||
return this.AddLines([.. points]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a series of line segments connecting the current point to the new points.
|
||||
/// </summary>
|
||||
/// <param name="points">The points.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddLines(params PointF[] points)
|
||||
{
|
||||
Guard.NotNull(points, nameof(points));
|
||||
return this.AddSegment(new LinearLineSegment(points));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the segment.
|
||||
/// </summary>
|
||||
/// <param name="segment">The segment.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddSegment(ILineSegment segment)
|
||||
{
|
||||
Guard.NotNull(segment, nameof(segment));
|
||||
|
||||
segment = segment.Transform(this.currentTransform);
|
||||
this.currentFigure.AddSegment(segment);
|
||||
this.currentPoint = segment.EndPoint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a quadratic bezier from the current point to the <paramref name="point"/>
|
||||
/// </summary>
|
||||
/// <param name="secondControlPoint">The second control point.</param>
|
||||
/// <param name="point">The point.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point)
|
||||
=> this.AddQuadraticBezier(this.currentPoint, secondControlPoint, point);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a quadratic bezier from the current point to the <paramref name="point"/>
|
||||
/// </summary>
|
||||
/// <param name="secondControlPoint">The second control point.</param>
|
||||
/// <param name="thirdControlPoint">The third control point.</param>
|
||||
/// <param name="point">The point.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point)
|
||||
=> this.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a quadratic bezier curve to the current figure joining the <paramref name="startPoint"/> point to the <paramref name="endPoint"/>.
|
||||
/// </summary>
|
||||
/// <param name="startPoint">The start point.</param>
|
||||
/// <param name="controlPoint">The control point1.</param>
|
||||
/// <param name="endPoint">The end point.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddQuadraticBezier(PointF startPoint, PointF controlPoint, PointF endPoint)
|
||||
{
|
||||
Vector2 startPointVector = startPoint;
|
||||
Vector2 controlPointVector = controlPoint;
|
||||
Vector2 endPointVector = endPoint;
|
||||
|
||||
Vector2 c1 = ((controlPointVector - startPointVector) * 2 / 3) + startPointVector;
|
||||
Vector2 c2 = ((controlPointVector - endPointVector) * 2 / 3) + endPointVector;
|
||||
|
||||
return this.AddCubicBezier(startPointVector, c1, c2, endPoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a cubic bezier curve to the current figure joining the <paramref name="startPoint"/> point to the <paramref name="endPoint"/>.
|
||||
/// </summary>
|
||||
/// <param name="startPoint">The start point.</param>
|
||||
/// <param name="controlPoint1">The control point1.</param>
|
||||
/// <param name="controlPoint2">The control point2.</param>
|
||||
/// <param name="endPoint">The end point.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddCubicBezier(PointF startPoint, PointF controlPoint1, PointF controlPoint2, PointF endPoint)
|
||||
=> this.AddSegment(new CubicBezierLineSegment(startPoint, controlPoint1, controlPoint2, endPoint));
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Adds an elliptical arc to the current figure. The arc curves from the last point to <paramref name="point"/>,
|
||||
/// choosing one of four possible routes: clockwise or counterclockwise, and smaller or larger.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The arc sweep is always less than 360 degrees. The method appends a line
|
||||
/// to the last point if either radii are zero, or if last point is equal to <paramref name="point"/>.
|
||||
/// In addition the method scales the radii to fit last point and <paramref name="point"/> if both
|
||||
/// are greater than zero but too small to describe an arc.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="radiusX">The x-radius of the ellipsis.</param>
|
||||
/// <param name="radiusY">The y-radius of the ellipsis.</param>
|
||||
/// <param name="rotation">The rotation along the X-axis; measured in degrees clockwise.</param>
|
||||
/// <param name="largeArc">
|
||||
/// The large arc flag, and is <see langword="false"/> if an arc spanning less than or equal to 180 degrees
|
||||
/// is chosen, or <see langword="true"/> if an arc spanning greater than 180 degrees is chosen.
|
||||
/// </param>
|
||||
/// <param name="sweep">
|
||||
/// The sweep flag, and is <see langword="false"/> if the line joining center to arc sweeps through decreasing
|
||||
/// angles, or <see langword="true"/> if it sweeps through increasing angles.
|
||||
/// </param>
|
||||
/// <param name="point">The end point of the arc.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, PointF point)
|
||||
=> this.AddArc(this.currentPoint, radiusX, radiusY, rotation, largeArc, sweep, point);
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Adds an elliptical arc to the current figure. The arc curves from the <paramref name="startPoint"/> to <paramref name="endPoint"/>,
|
||||
/// choosing one of four possible routes: clockwise or counterclockwise, and smaller or larger.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The arc sweep is always less than 360 degrees. The method appends a line
|
||||
/// to the last point if either radii are zero, or if last point is equal to <paramref name="endPoint"/>.
|
||||
/// In addition the method scales the radii to fit last point and <paramref name="endPoint"/> if both
|
||||
/// are greater than zero but too small to describe an arc.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="startPoint">The start point of the arc.</param>
|
||||
/// <param name="radiusX">The x-radius of the ellipsis.</param>
|
||||
/// <param name="radiusY">The y-radius of the ellipsis.</param>
|
||||
/// <param name="rotation">The rotation along the X-axis; measured in degrees clockwise.</param>
|
||||
/// <param name="largeArc">
|
||||
/// The large arc flag, and is <see langword="false"/> if an arc spanning less than or equal to 180 degrees
|
||||
/// is chosen, or <see langword="true"/> if an arc spanning greater than 180 degrees is chosen.
|
||||
/// </param>
|
||||
/// <param name="sweep">
|
||||
/// The sweep flag, and is <see langword="false"/> if the line joining center to arc sweeps through decreasing
|
||||
/// angles, or <see langword="true"/> if it sweeps through increasing angles.
|
||||
/// </param>
|
||||
/// <param name="endPoint">The end point of the arc.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddArc(PointF startPoint, float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, PointF endPoint)
|
||||
=> this.AddSegment(new ArcLineSegment(startPoint, endPoint, new SizeF(radiusX, radiusY), rotation, largeArc, sweep));
|
||||
|
||||
/// <summary>
|
||||
/// Adds an elliptical arc to the current figure.
|
||||
/// </summary>
|
||||
/// <param name="rectangle">A <see cref="RectangleF"/> that represents the rectangular bounds of the ellipse from which the arc is taken.</param>
|
||||
/// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param>
|
||||
/// <param name="startAngle">
|
||||
/// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle).
|
||||
/// </param>
|
||||
/// <param name="sweepAngle">The angle between <paramref name="startAngle"/> and the end of the arc.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddArc(RectangleF rectangle, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.AddArc((rectangle.Right + rectangle.Left) / 2, (rectangle.Bottom + rectangle.Top) / 2, rectangle.Width / 2, rectangle.Height / 2, rotation, startAngle, sweepAngle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an elliptical arc to the current figure.
|
||||
/// </summary>
|
||||
/// <param name="rectangle">A <see cref="Rectangle"/> that represents the rectangular bounds of the ellipse from which the arc is taken.</param>
|
||||
/// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param>
|
||||
/// <param name="startAngle">
|
||||
/// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle).
|
||||
/// </param>
|
||||
/// <param name="sweepAngle">The angle between <paramref name="startAngle"/> and the end of the arc.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddArc(Rectangle rectangle, int rotation, int startAngle, int sweepAngle)
|
||||
=> this.AddArc((RectangleF)rectangle, rotation, startAngle, sweepAngle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an elliptical arc to the current figure.
|
||||
/// </summary>
|
||||
/// <param name="center">The center <see cref="PointF"/> of the ellipse from which the arc is taken.</param>
|
||||
/// <param name="radiusX">The x-radius of the ellipsis.</param>
|
||||
/// <param name="radiusY">The y-radius of the ellipsis.</param>
|
||||
/// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param>
|
||||
/// <param name="startAngle">
|
||||
/// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle).
|
||||
/// </param>
|
||||
/// <param name="sweepAngle">The angle between <paramref name="startAngle"/> and the end of the arc.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddArc(PointF center, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.AddArc(center.X, center.Y, radiusX, radiusY, rotation, startAngle, sweepAngle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an elliptical arc to the current figure.
|
||||
/// </summary>
|
||||
/// <param name="center">The center <see cref="Point"/> of the ellipse from which the arc is taken.</param>
|
||||
/// <param name="radiusX">The x-radius of the ellipsis.</param>
|
||||
/// <param name="radiusY">The y-radius of the ellipsis.</param>
|
||||
/// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param>
|
||||
/// <param name="startAngle">
|
||||
/// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle).
|
||||
/// </param>
|
||||
/// <param name="sweepAngle">The angle between <paramref name="startAngle"/> and the end of the arc.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddArc(Point center, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle)
|
||||
=> this.AddArc((PointF)center, radiusX, radiusY, rotation, startAngle, sweepAngle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an elliptical arc to the current figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the center point of the ellipse from which the arc is taken.</param>
|
||||
/// <param name="y">The y-coordinate of the center point of the ellipse from which the arc is taken.</param>
|
||||
/// <param name="radiusX">The x-radius of the ellipsis.</param>
|
||||
/// <param name="radiusY">The y-radius of the ellipsis.</param>
|
||||
/// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param>
|
||||
/// <param name="startAngle">
|
||||
/// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle).
|
||||
/// </param>
|
||||
/// <param name="sweepAngle">The angle between <paramref name="startAngle"/> and the end of the arc.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddArc(int x, int y, int radiusX, int radiusY, int rotation, int startAngle, int sweepAngle)
|
||||
=> this.AddSegment(new ArcLineSegment(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Adds an elliptical arc to the current figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the center point of the ellipse from which the arc is taken.</param>
|
||||
/// <param name="y">The y-coordinate of the center point of the ellipse from which the arc is taken.</param>
|
||||
/// <param name="radiusX">The x-radius of the ellipsis.</param>
|
||||
/// <param name="radiusY">The y-radius of the ellipsis.</param>
|
||||
/// <param name="rotation">The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse.</param>
|
||||
/// <param name="startAngle">
|
||||
/// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle).
|
||||
/// </param>
|
||||
/// <param name="sweepAngle">The angle between <paramref name="startAngle"/> and the end of the arc.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddArc(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.AddSegment(new ArcLineSegment(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a pie sector to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="center">The center point of the pie sector.</param>
|
||||
/// <param name="radius">The x and y radii of the pie ellipse.</param>
|
||||
/// <param name="rotation">The ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">The pie start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">The pie sweep angle in degrees.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddPie(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
{
|
||||
_ = this.StartFigure();
|
||||
|
||||
foreach (ILineSegment segment in new PiePolygon(center, radius, rotation, startAngle, sweepAngle).LineSegments)
|
||||
{
|
||||
_ = this.AddSegment(segment);
|
||||
}
|
||||
|
||||
return this.CloseFigure();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a pie sector to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="center">The center point of the pie sector.</param>
|
||||
/// <param name="radius">The x and y radii of the pie ellipse.</param>
|
||||
/// <param name="startAngle">The pie start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">The pie sweep angle in degrees.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddPie(PointF center, SizeF radius, float startAngle, float sweepAngle)
|
||||
=> this.AddPie(center, radius, 0F, startAngle, sweepAngle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a pie sector to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the pie center.</param>
|
||||
/// <param name="y">The y-coordinate of the pie center.</param>
|
||||
/// <param name="radiusX">The x-radius of the pie ellipse.</param>
|
||||
/// <param name="radiusY">The y-radius of the pie ellipse.</param>
|
||||
/// <param name="rotation">The ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">The pie start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">The pie sweep angle in degrees.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddPie(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.AddPie(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a pie sector to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the pie center.</param>
|
||||
/// <param name="y">The y-coordinate of the pie center.</param>
|
||||
/// <param name="radiusX">The x-radius of the pie ellipse.</param>
|
||||
/// <param name="radiusY">The y-radius of the pie ellipse.</param>
|
||||
/// <param name="startAngle">The pie start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">The pie sweep angle in degrees.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddPie(float x, float y, float radiusX, float radiusY, float startAngle, float sweepAngle)
|
||||
=> this.AddPie(x, y, radiusX, radiusY, 0F, startAngle, sweepAngle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rectangle to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="rectangle">The rectangle bounds.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRectangle(RectangleF rectangle)
|
||||
=> this.AddRectangle(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rectangle to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="rectangle">The rectangle bounds.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRectangle(Rectangle rectangle)
|
||||
=> this.AddRectangle((RectangleF)rectangle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rectangle to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the rectangle.</param>
|
||||
/// <param name="y">The y-coordinate of the rectangle.</param>
|
||||
/// <param name="width">The rectangle width.</param>
|
||||
/// <param name="height">The rectangle height.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRectangle(float x, float y, float width, float height)
|
||||
=> this.AddPolygon(
|
||||
new PointF(x, y),
|
||||
new PointF(x + width, y),
|
||||
new PointF(x + width, y + height),
|
||||
new PointF(x, y + height));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rounded rectangle to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="rectangle">The rectangle bounds.</param>
|
||||
/// <param name="radius">The x and y radius of each corner.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRoundedRectangle(RectangleF rectangle, float radius)
|
||||
=> this.AddRoundedRectangle(rectangle, new SizeF(radius, radius));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rounded rectangle to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="rectangle">The rectangle bounds.</param>
|
||||
/// <param name="radius">The x and y radii of each corner.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRoundedRectangle(RectangleF rectangle, SizeF radius)
|
||||
{
|
||||
_ = this.StartFigure();
|
||||
|
||||
foreach (ILineSegment segment in new RoundedRectanglePolygon(rectangle, radius).LineSegments)
|
||||
{
|
||||
_ = this.AddSegment(segment);
|
||||
}
|
||||
|
||||
return this.CloseFigure();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rounded rectangle to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="rectangle">The rectangle bounds.</param>
|
||||
/// <param name="radius">The x and y radius of each corner.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRoundedRectangle(Rectangle rectangle, float radius)
|
||||
=> this.AddRoundedRectangle((RectangleF)rectangle, radius);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rounded rectangle to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="rectangle">The rectangle bounds.</param>
|
||||
/// <param name="radius">The x and y radii of each corner.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRoundedRectangle(Rectangle rectangle, SizeF radius)
|
||||
=> this.AddRoundedRectangle((RectangleF)rectangle, radius);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rounded rectangle to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the rectangle.</param>
|
||||
/// <param name="y">The y-coordinate of the rectangle.</param>
|
||||
/// <param name="width">The rectangle width.</param>
|
||||
/// <param name="height">The rectangle height.</param>
|
||||
/// <param name="radius">The x and y radius of each corner.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRoundedRectangle(float x, float y, float width, float height, float radius)
|
||||
=> this.AddRoundedRectangle(new RectangleF(x, y, width, height), radius);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rounded rectangle to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the rectangle.</param>
|
||||
/// <param name="y">The y-coordinate of the rectangle.</param>
|
||||
/// <param name="width">The rectangle width.</param>
|
||||
/// <param name="height">The rectangle height.</param>
|
||||
/// <param name="radius">The x and y radii of each corner.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRoundedRectangle(float x, float y, float width, float height, SizeF radius)
|
||||
=> this.AddRoundedRectangle(new RectangleF(x, y, width, height), radius);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a polygon to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="points">The polygon vertices.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddPolygon(IEnumerable<PointF> points)
|
||||
{
|
||||
Guard.NotNull(points, nameof(points));
|
||||
return this.AddPolygon([.. points]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a polygon to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="points">The polygon vertices.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddPolygon(params PointF[] points)
|
||||
{
|
||||
Guard.NotNull(points, nameof(points));
|
||||
|
||||
_ = this.StartFigure();
|
||||
_ = this.AddSegment(new LinearLineSegment(points));
|
||||
return this.CloseFigure();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a regular polygon to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="center">The center point of the polygon.</param>
|
||||
/// <param name="vertices">The number of polygon vertices.</param>
|
||||
/// <param name="radius">The polygon radius.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRegularPolygon(PointF center, int vertices, float radius)
|
||||
=> this.AddRegularPolygon(center, vertices, radius, 0F);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a regular polygon to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="center">The center point of the polygon.</param>
|
||||
/// <param name="vertices">The number of polygon vertices.</param>
|
||||
/// <param name="radius">The polygon radius.</param>
|
||||
/// <param name="angle">The polygon rotation angle in degrees.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRegularPolygon(PointF center, int vertices, float radius, float angle)
|
||||
{
|
||||
_ = this.StartFigure();
|
||||
|
||||
foreach (ILineSegment segment in new RegularPolygon(center, vertices, radius, angle).LineSegments)
|
||||
{
|
||||
_ = this.AddSegment(segment);
|
||||
}
|
||||
|
||||
return this.CloseFigure();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a regular polygon to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the polygon center.</param>
|
||||
/// <param name="y">The y-coordinate of the polygon center.</param>
|
||||
/// <param name="vertices">The number of polygon vertices.</param>
|
||||
/// <param name="radius">The polygon radius.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRegularPolygon(float x, float y, int vertices, float radius)
|
||||
=> this.AddRegularPolygon(new PointF(x, y), vertices, radius);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a regular polygon to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the polygon center.</param>
|
||||
/// <param name="y">The y-coordinate of the polygon center.</param>
|
||||
/// <param name="vertices">The number of polygon vertices.</param>
|
||||
/// <param name="radius">The polygon radius.</param>
|
||||
/// <param name="angle">The polygon rotation angle in degrees.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddRegularPolygon(float x, float y, int vertices, float radius, float angle)
|
||||
=> this.AddRegularPolygon(new PointF(x, y), vertices, radius, angle);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a star to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="center">The center point of the star.</param>
|
||||
/// <param name="prongs">The number of star prongs.</param>
|
||||
/// <param name="innerRadii">The inner star radius.</param>
|
||||
/// <param name="outerRadii">The outer star radius.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddStar(PointF center, int prongs, float innerRadii, float outerRadii)
|
||||
=> this.AddStar(center, prongs, innerRadii, outerRadii, 0F);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a star to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="center">The center point of the star.</param>
|
||||
/// <param name="prongs">The number of star prongs.</param>
|
||||
/// <param name="innerRadii">The inner star radius.</param>
|
||||
/// <param name="outerRadii">The outer star radius.</param>
|
||||
/// <param name="angle">The star rotation angle in degrees.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddStar(PointF center, int prongs, float innerRadii, float outerRadii, float angle)
|
||||
{
|
||||
_ = this.StartFigure();
|
||||
|
||||
foreach (ILineSegment segment in new StarPolygon(center, prongs, innerRadii, outerRadii, angle).LineSegments)
|
||||
{
|
||||
_ = this.AddSegment(segment);
|
||||
}
|
||||
|
||||
return this.CloseFigure();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a star to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the star center.</param>
|
||||
/// <param name="y">The y-coordinate of the star center.</param>
|
||||
/// <param name="prongs">The number of star prongs.</param>
|
||||
/// <param name="innerRadii">The inner star radius.</param>
|
||||
/// <param name="outerRadii">The outer star radius.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddStar(float x, float y, int prongs, float innerRadii, float outerRadii)
|
||||
=> this.AddStar(new PointF(x, y), prongs, innerRadii, outerRadii);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a star to the current path as a closed figure.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the star center.</param>
|
||||
/// <param name="y">The y-coordinate of the star center.</param>
|
||||
/// <param name="prongs">The number of star prongs.</param>
|
||||
/// <param name="innerRadii">The inner star radius.</param>
|
||||
/// <param name="outerRadii">The outer star radius.</param>
|
||||
/// <param name="angle">The star rotation angle in degrees.</param>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder AddStar(float x, float y, int prongs, float innerRadii, float outerRadii, float angle)
|
||||
=> this.AddStar(new PointF(x, y), prongs, innerRadii, outerRadii, angle);
|
||||
|
||||
/// <summary>
|
||||
/// Starts a new figure but leaves the previous one open.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder StartFigure()
|
||||
{
|
||||
if (!this.currentFigure.IsEmpty)
|
||||
{
|
||||
this.currentFigure = new Figure();
|
||||
this.figures.Add(this.currentFigure);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.currentFigure.IsClosed = false;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the current figure.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder CloseFigure()
|
||||
{
|
||||
this.currentFigure.IsClosed = true;
|
||||
_ = this.StartFigure();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the current figure.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder CloseAllFigures()
|
||||
{
|
||||
foreach (Figure f in this.figures)
|
||||
{
|
||||
f.IsClosed = true;
|
||||
}
|
||||
|
||||
_ = this.CloseFigure();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a complex polygon from the current working set of working operations.
|
||||
/// </summary>
|
||||
/// <returns>The current set of operations as a complex polygon</returns>
|
||||
public IPath Build()
|
||||
{
|
||||
IPath[] paths = [.. this.figures.Where(x => !x.IsEmpty).Select(x => x.Build())];
|
||||
if (paths.Length == 1)
|
||||
{
|
||||
return paths[0];
|
||||
}
|
||||
|
||||
return new ComplexPolygon(paths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets this instance, clearing any drawn paths and resetting any transforms.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="PathBuilder"/>.</returns>
|
||||
public PathBuilder Reset()
|
||||
{
|
||||
this.Clear();
|
||||
_ = this.ResetTransform();
|
||||
this.currentPoint = default;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all drawn paths, Leaving any applied transforms.
|
||||
/// </summary>
|
||||
[MemberNotNull(nameof(currentFigure))]
|
||||
public void Clear()
|
||||
{
|
||||
this.currentFigure = new Figure();
|
||||
this.figures.Clear();
|
||||
this.figures.Add(this.currentFigure);
|
||||
}
|
||||
|
||||
private class Figure
|
||||
{
|
||||
private readonly List<ILineSegment> segments = [];
|
||||
|
||||
public bool IsClosed { get; set; }
|
||||
|
||||
public bool IsEmpty => this.segments.Count == 0;
|
||||
|
||||
public void AddSegment(ILineSegment segment) => this.segments.Add(segment);
|
||||
|
||||
public IPath Build()
|
||||
=> this.IsClosed
|
||||
? new Polygon([.. this.segments], true)
|
||||
: new Path(this.segments.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
89
ImageSharp.Drawing/PathCollection.cs
Normal file
89
ImageSharp.Drawing/PathCollection.cs
Normal file
@ -0,0 +1,89 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// A aggregate of <see cref="IPath"/>s to apply common operations to them.
|
||||
/// </summary>
|
||||
/// <seealso cref="IPath" />
|
||||
public class PathCollection : IPathCollection
|
||||
{
|
||||
private readonly IPath[] paths;
|
||||
private RectangleF? bounds;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathCollection"/> class.
|
||||
/// </summary>
|
||||
/// <param name="paths">The collection of paths</param>
|
||||
public PathCollection(IEnumerable<IPath> paths)
|
||||
: this(GetPathArray(paths))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathCollection"/> class.
|
||||
/// </summary>
|
||||
/// <param name="paths">The collection of paths</param>
|
||||
public PathCollection(params IPath[] paths)
|
||||
{
|
||||
Guard.NotNull(paths, nameof(paths));
|
||||
this.paths = paths;
|
||||
|
||||
if (paths.Length == 0)
|
||||
{
|
||||
this.bounds = new RectangleF(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public RectangleF Bounds => this.bounds ??= this.CalcBounds();
|
||||
|
||||
private RectangleF CalcBounds()
|
||||
{
|
||||
float minX, minY, maxX, maxY;
|
||||
minX = minY = float.MaxValue;
|
||||
maxX = maxY = float.MinValue;
|
||||
|
||||
foreach (IPath path in this.paths)
|
||||
{
|
||||
RectangleF bounds = path.Bounds;
|
||||
minX = Math.Min(bounds.Left, minX);
|
||||
minY = Math.Min(bounds.Top, minY);
|
||||
maxX = Math.Max(bounds.Right, maxX);
|
||||
maxY = Math.Max(bounds.Bottom, maxY);
|
||||
}
|
||||
|
||||
return new RectangleF(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<IPath> GetEnumerator() => ((IEnumerable<IPath>)this.paths).GetEnumerator();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPathCollection Transform(Matrix4x4 matrix)
|
||||
{
|
||||
IPath[] result = new IPath[this.paths.Length];
|
||||
|
||||
for (int i = 0; i < this.paths.Length && i < result.Length; i++)
|
||||
{
|
||||
result[i] = this.paths[i].Transform(matrix);
|
||||
}
|
||||
|
||||
return new PathCollection(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<IPath>)this.paths).GetEnumerator();
|
||||
|
||||
private static IPath[] GetPathArray(IEnumerable<IPath> paths)
|
||||
{
|
||||
Guard.NotNull(paths, nameof(paths));
|
||||
return paths as IPath[] ?? [.. paths];
|
||||
}
|
||||
}
|
||||
}
|
||||
43
ImageSharp.Drawing/PathExtensions.Internal.cs
Normal file
43
ImageSharp.Drawing/PathExtensions.Internal.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <content>
|
||||
/// Convenience methods that can be applied to shapes and paths.
|
||||
/// </content>
|
||||
public static partial class PathExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a path with the segment order reversed.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to reverse.</param>
|
||||
/// <returns>The reversed <see cref="IPath"/>.</returns>
|
||||
internal static IPath Reverse(this IPath path)
|
||||
{
|
||||
// TODO. Make this a void. We can reverse the segments in place and then reverse the points in place as well.
|
||||
IEnumerable<LinearLineSegment> segments = path.Flatten().Select(static p => new LinearLineSegment(ReversePoints(p.Points.Span)));
|
||||
bool closed = false;
|
||||
if (path is ISimplePath sp)
|
||||
{
|
||||
closed = sp.IsClosed;
|
||||
}
|
||||
|
||||
return closed ? new Polygon(segments) : new Path(segments);
|
||||
}
|
||||
|
||||
private static PointF[] ReversePoints(ReadOnlySpan<PointF> points)
|
||||
{
|
||||
PointF[] reversed = new PointF[points.Length];
|
||||
for (int i = 0; i < reversed.Length; i++)
|
||||
{
|
||||
reversed[i] = points[points.Length - 1 - i];
|
||||
}
|
||||
|
||||
return reversed;
|
||||
}
|
||||
}
|
||||
}
|
||||
219
ImageSharp.Drawing/PathExtensions.cs
Normal file
219
ImageSharp.Drawing/PathExtensions.cs
Normal file
@ -0,0 +1,219 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Convenience methods that can be applied to shapes and paths.
|
||||
/// </summary>
|
||||
public static partial class PathExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a path rotated by the specified radians around its center.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to rotate.</param>
|
||||
/// <param name="radians">The radians to rotate the path.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a rotate transform applied.</returns>
|
||||
public static IPathCollection Rotate(this IPathCollection path, float radians)
|
||||
=> path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds))));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path rotated by the specified degrees around its center.
|
||||
/// </summary>
|
||||
/// <param name="shape">The path to rotate.</param>
|
||||
/// <param name="degree">The degree to rotate the path.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a rotate transform applied.</returns>
|
||||
public static IPathCollection RotateDegree(this IPathCollection shape, float degree)
|
||||
=> shape.Rotate(GeometryUtilities.DegreeToRadian(degree));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path translated by the supplied position
|
||||
/// </summary>
|
||||
/// <param name="path">The path to translate.</param>
|
||||
/// <param name="position">The translation position.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a translate transform applied.</returns>
|
||||
public static IPathCollection Translate(this IPathCollection path, PointF position)
|
||||
=> path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path translated by the supplied position
|
||||
/// </summary>
|
||||
/// <param name="path">The path to translate.</param>
|
||||
/// <param name="x">The amount to translate along the X axis.</param>
|
||||
/// <param name="y">The amount to translate along the Y axis.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a translate transform applied.</returns>
|
||||
public static IPathCollection Translate(this IPathCollection path, float x, float y)
|
||||
=> path.Translate(new PointF(x, y));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path translated by the supplied position
|
||||
/// </summary>
|
||||
/// <param name="path">The path to translate.</param>
|
||||
/// <param name="scaleX">The amount to scale along the X axis.</param>
|
||||
/// <param name="scaleY">The amount to scale along the Y axis.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a translate transform applied.</returns>
|
||||
public static IPathCollection Scale(this IPathCollection path, float scaleX, float scaleY)
|
||||
=> path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0)));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path translated by the supplied position
|
||||
/// </summary>
|
||||
/// <param name="path">The path to translate.</param>
|
||||
/// <param name="scale">The amount to scale along both the x and y axis.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a translate transform applied.</returns>
|
||||
public static IPathCollection Scale(this IPathCollection path, float scale)
|
||||
=> path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0)));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path rotated by the specified radians around its center.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to rotate.</param>
|
||||
/// <param name="radians">The radians to rotate the path.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a rotate transform applied.</returns>
|
||||
public static IPath Rotate(this IPath path, float radians)
|
||||
=> path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds))));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path rotated by the specified degrees around its center.
|
||||
/// </summary>
|
||||
/// <param name="shape">The path to rotate.</param>
|
||||
/// <param name="degree">The degree to rotate the path.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a rotate transform applied.</returns>
|
||||
public static IPath RotateDegree(this IPath shape, float degree)
|
||||
=> shape.Rotate(GeometryUtilities.DegreeToRadian(degree));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path translated by the supplied position
|
||||
/// </summary>
|
||||
/// <param name="path">The path to translate.</param>
|
||||
/// <param name="position">The translation position.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a translate transform applied.</returns>
|
||||
public static IPath Translate(this IPath path, PointF position)
|
||||
=> path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path translated by the supplied position
|
||||
/// </summary>
|
||||
/// <param name="path">The path to translate.</param>
|
||||
/// <param name="x">The amount to translate along the X axis.</param>
|
||||
/// <param name="y">The amount to translate along the Y axis.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a translate transform applied.</returns>
|
||||
public static IPath Translate(this IPath path, float x, float y)
|
||||
=> path.Translate(new Vector2(x, y));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path translated by the supplied position
|
||||
/// </summary>
|
||||
/// <param name="path">The path to translate.</param>
|
||||
/// <param name="scaleX">The amount to scale along the X axis.</param>
|
||||
/// <param name="scaleY">The amount to scale along the Y axis.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a translate transform applied.</returns>
|
||||
public static IPath Scale(this IPath path, float scaleX, float scaleY)
|
||||
=> path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0)));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a path translated by the supplied position
|
||||
/// </summary>
|
||||
/// <param name="path">The path to translate.</param>
|
||||
/// <param name="scale">The amount to scale along both the x and y axis.</param>
|
||||
/// <returns>A <see cref="IPath"/> with a translate transform applied.</returns>
|
||||
public static IPath Scale(this IPath path, float scale)
|
||||
=> path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0)));
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the approximate length of the path as though each segment were unrolled into a line.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to compute the length for.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="float"/> representing the unrolled length.
|
||||
/// For closed paths, the length includes an implicit closing segment.
|
||||
/// </returns>
|
||||
public static float ComputeLength(this IPath path)
|
||||
{
|
||||
float dist = 0;
|
||||
foreach (ISimplePath s in path.Flatten())
|
||||
{
|
||||
ReadOnlySpan<PointF> points = s.Points.Span;
|
||||
if (points.Length < 2)
|
||||
{
|
||||
// Only a single point
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 1; i < points.Length; i++)
|
||||
{
|
||||
dist += Vector2.Distance(points[i - 1], points[i]);
|
||||
}
|
||||
|
||||
if (s.IsClosed)
|
||||
{
|
||||
dist += Vector2.Distance(points[0], points[^1]);
|
||||
}
|
||||
}
|
||||
|
||||
return dist;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the total area of all paths in the specified collection.
|
||||
/// </summary>
|
||||
/// <param name="paths">A collection of paths for which to compute the combined area. Cannot be null.</param>
|
||||
/// <returns>
|
||||
/// The total area, in square units, enclosed by all paths in the collection.
|
||||
/// </returns>
|
||||
public static float ComputeArea(this IPathCollection paths)
|
||||
{
|
||||
float area = 0;
|
||||
foreach (IPath path in paths)
|
||||
{
|
||||
area += path.ComputeArea();
|
||||
}
|
||||
|
||||
return area;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the total area enclosed by the specified path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method sums the areas of all subpaths within the path. Subpaths with fewer than three
|
||||
/// points are ignored, as they do not form a closed region. The result is always non-negative, regardless of the
|
||||
/// winding direction of the subpaths.
|
||||
/// </remarks>
|
||||
/// <param name="path">
|
||||
/// The path for which to compute the enclosed area. Must contain at least one subpath with three or more points to
|
||||
/// contribute to the area calculation.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The total area, in square units, enclosed by all subpaths of the path. Returns 0 if the path does not contain
|
||||
/// any subpaths with at least three points.
|
||||
/// </returns>
|
||||
public static float ComputeArea(this IPath path)
|
||||
{
|
||||
float area = 0;
|
||||
foreach (ISimplePath s in path.Flatten())
|
||||
{
|
||||
ReadOnlySpan<PointF> points = s.Points.Span;
|
||||
if (points.Length < 3)
|
||||
{
|
||||
// Not enough points to form an area
|
||||
continue;
|
||||
}
|
||||
|
||||
float subArea = 0;
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
{
|
||||
PointF p1 = points[i];
|
||||
PointF p2 = points[(i + 1) % points.Length];
|
||||
subArea += (p1.X * p2.Y) - (p2.X * p1.Y);
|
||||
}
|
||||
|
||||
area += MathF.Abs(subArea) * .5F;
|
||||
}
|
||||
|
||||
return area;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
ImageSharp.Drawing/PathTypes.cs
Normal file
25
ImageSharp.Drawing/PathTypes.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Describes the different type of paths.
|
||||
/// </summary>
|
||||
public enum PathTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes a path containing a single simple open path
|
||||
/// </summary>
|
||||
Open,
|
||||
|
||||
/// <summary>
|
||||
/// Denotes a path describing a single simple closed shape
|
||||
/// </summary>
|
||||
Closed,
|
||||
|
||||
/// <summary>
|
||||
/// Denotes a path containing one or more child paths that could be open or closed.
|
||||
/// </summary>
|
||||
Mixed
|
||||
}
|
||||
}
|
||||
120
ImageSharp.Drawing/PiePolygon.cs
Normal file
120
ImageSharp.Drawing/PiePolygon.cs
Normal file
@ -0,0 +1,120 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// A pie sector polygon defined by a center point, radii, rotation, and arc sweep.
|
||||
/// </summary>
|
||||
public sealed class PiePolygon : Polygon
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PiePolygon"/> class.
|
||||
/// </summary>
|
||||
/// <param name="center">The center point of the pie sector.</param>
|
||||
/// <param name="radius">The x and y radii of the pie ellipse.</param>
|
||||
/// <param name="rotation">The ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">The pie start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">The pie sweep angle in degrees.</param>
|
||||
public PiePolygon(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
: base(CreateSegments(center, radius, rotation, startAngle, sweepAngle))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PiePolygon"/> class.
|
||||
/// </summary>
|
||||
/// <param name="center">The center point of the pie sector.</param>
|
||||
/// <param name="radius">The x and y radii of the pie ellipse.</param>
|
||||
/// <param name="startAngle">The pie start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">The pie sweep angle in degrees.</param>
|
||||
public PiePolygon(PointF center, SizeF radius, float startAngle, float sweepAngle)
|
||||
: this(center, radius, 0F, startAngle, sweepAngle)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PiePolygon"/> class.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the pie center.</param>
|
||||
/// <param name="y">The y-coordinate of the pie center.</param>
|
||||
/// <param name="radiusX">The x-radius of the pie ellipse.</param>
|
||||
/// <param name="radiusY">The y-radius of the pie ellipse.</param>
|
||||
/// <param name="rotation">The ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">The pie start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">The pie sweep angle in degrees.</param>
|
||||
public PiePolygon(float x, float y, float radiusX, float radiusY, float rotation, float startAngle, float sweepAngle)
|
||||
: this(new PointF(x, y), new SizeF(radiusX, radiusY), rotation, startAngle, sweepAngle)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PiePolygon"/> class.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the pie center.</param>
|
||||
/// <param name="y">The y-coordinate of the pie center.</param>
|
||||
/// <param name="radiusX">The x-radius of the pie ellipse.</param>
|
||||
/// <param name="radiusY">The y-radius of the pie ellipse.</param>
|
||||
/// <param name="startAngle">The pie start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">The pie sweep angle in degrees.</param>
|
||||
public PiePolygon(float x, float y, float radiusX, float radiusY, float startAngle, float sweepAngle)
|
||||
: this(x, y, radiusX, radiusY, 0F, startAngle, sweepAngle)
|
||||
{
|
||||
}
|
||||
|
||||
private PiePolygon(ILineSegment[] segments)
|
||||
: base(segments, true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPath Transform(Matrix4x4 matrix)
|
||||
{
|
||||
if (matrix.IsIdentity)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
ILineSegment[] segments = new ILineSegment[this.LineSegments.Count];
|
||||
|
||||
for (int i = 0; i < segments.Length; i++)
|
||||
{
|
||||
segments[i] = this.LineSegments[i].Transform(matrix);
|
||||
}
|
||||
|
||||
return new PiePolygon(segments);
|
||||
}
|
||||
|
||||
private static ILineSegment[] CreateSegments(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
{
|
||||
Guard.MustBeGreaterThan(radius.Width, 0, "radiusX");
|
||||
Guard.MustBeGreaterThan(radius.Height, 0, "radiusY");
|
||||
|
||||
PointF arcStart = GetArcPoint(center, radius, rotation, startAngle);
|
||||
ArcLineSegment arc = new(center, radius, rotation, startAngle, sweepAngle);
|
||||
|
||||
return
|
||||
[
|
||||
new LinearLineSegment(center, arcStart),
|
||||
arc,
|
||||
new LinearLineSegment(arc.EndPoint, center)
|
||||
];
|
||||
}
|
||||
|
||||
private static PointF GetArcPoint(PointF center, SizeF radius, float rotation, float angle)
|
||||
{
|
||||
float rotationRadians = rotation * (MathF.PI / 180F);
|
||||
float angleRadians = angle * (MathF.PI / 180F);
|
||||
float cosRotation = MathF.Cos(rotationRadians);
|
||||
float sinRotation = MathF.Sin(rotationRadians);
|
||||
float cosAngle = MathF.Cos(angleRadians);
|
||||
float sinAngle = MathF.Sin(angleRadians);
|
||||
|
||||
return new PointF(
|
||||
center.X + (radius.Width * cosRotation * cosAngle) - (radius.Height * sinRotation * sinAngle),
|
||||
center.Y + (radius.Width * sinRotation * cosAngle) + (radius.Height * cosRotation * sinAngle));
|
||||
}
|
||||
}
|
||||
}
|
||||
25
ImageSharp.Drawing/PointOrientation.cs
Normal file
25
ImageSharp.Drawing/PointOrientation.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// Represents the orientation of a point from a line.
|
||||
/// </summary>
|
||||
internal enum PointOrientation
|
||||
{
|
||||
/// <summary>
|
||||
/// The point is collinear.
|
||||
/// </summary>
|
||||
Collinear = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The point is clockwise.
|
||||
/// </summary>
|
||||
Clockwise = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The point is counter-clockwise.
|
||||
/// </summary>
|
||||
Counterclockwise = 2
|
||||
}
|
||||
}
|
||||
98
ImageSharp.Drawing/Polygon.cs
Normal file
98
ImageSharp.Drawing/Polygon.cs
Normal file
@ -0,0 +1,98 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing {
|
||||
/// <summary>
|
||||
/// A shape made up of a single closed path made up of one of more <see cref="ILineSegment"/>s
|
||||
/// </summary>
|
||||
public class Polygon : Path
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Polygon"/> class.
|
||||
/// </summary>
|
||||
/// <param name="points">The collection of points; processed as a series of linear line segments.</param>
|
||||
public Polygon(PointF[] points)
|
||||
: this(new LinearLineSegment(points))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Polygon"/> class.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments.</param>
|
||||
public Polygon(params ILineSegment[] segments)
|
||||
: base(segments)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Polygon"/> class.
|
||||
/// </summary>
|
||||
/// <param name="segments">The segments.</param>
|
||||
public Polygon(IEnumerable<ILineSegment> segments)
|
||||
: base(segments)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Polygon" /> class.
|
||||
/// </summary>
|
||||
/// <param name="segment">The segment.</param>
|
||||
public Polygon(ILineSegment segment)
|
||||
: base(segment)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Polygon"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
internal Polygon(Path path)
|
||||
: base(path)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Polygon"/> class using the specified line segments.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If owned is set to <see langword="true"/>, modifications to the segments array after construction may affect
|
||||
/// the Polygon instance. If owned is <see langword="false"/>, the segments are copied to ensure the Polygon is not affected by
|
||||
/// external changes.
|
||||
/// </remarks>
|
||||
/// <param name="segments">An array of line segments that define the edges of the polygon. The order of segments determines the shape of
|
||||
/// the polygon.</param>
|
||||
/// <param name="owned">
|
||||
/// <see langword="true"/> to indicate that the Polygon instance takes ownership of the segments array;
|
||||
/// <see langword="false"/> to create a copy of the array.
|
||||
/// </param>
|
||||
internal Polygon(ILineSegment[] segments, bool owned)
|
||||
: base(owned ? segments : [.. segments])
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsClosed => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPath Transform(Matrix4x4 matrix)
|
||||
{
|
||||
if (matrix.IsIdentity)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
ILineSegment[] segments = new ILineSegment[this.LineSegments.Count];
|
||||
|
||||
for (int i = 0; i < segments.Length; i++)
|
||||
{
|
||||
segments[i] = this.LineSegments[i].Transform(matrix);
|
||||
}
|
||||
|
||||
return new Polygon(segments, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
114
ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs
Normal file
114
ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs
Normal file
@ -0,0 +1,114 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.PolygonClipper;
|
||||
using System.Collections.Generic;
|
||||
using PCPolygon = SixLabors.PolygonClipper.Polygon;
|
||||
using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.PolygonGeometry {
|
||||
/// <summary>
|
||||
/// Generates clipped shapes from one or more input paths using polygon boolean operations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class provides a high-level wrapper around the low-level <see cref="PolygonClipperAction"/>.
|
||||
/// It accumulates subject and clip polygons, applies the specified <see cref="BooleanOperation"/>,
|
||||
/// and converts the resulting polygon contours back into <see cref="ComplexPolygon"/> instances suitable
|
||||
/// for rendering or further processing.
|
||||
/// </remarks>
|
||||
internal static class ClippedShapeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the final clipped shapes from the previously provided subject and clip paths.
|
||||
/// </summary>
|
||||
/// <param name="operation">
|
||||
/// The boolean operation to perform, such as <see cref="BooleanOperation.Union"/>,
|
||||
/// <see cref="BooleanOperation.Intersection"/>, or <see cref="BooleanOperation.Difference"/>.
|
||||
/// </param>
|
||||
/// <param name="subject">The subject path.</param>
|
||||
/// <param name="clip">The clipping paths.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="ComplexPolygon"/> representing the result of the boolean operation.
|
||||
/// </returns>
|
||||
public static ComplexPolygon GenerateClippedShapes(
|
||||
BooleanOperation operation,
|
||||
IPath subject,
|
||||
IEnumerable<IPath> clip)
|
||||
{
|
||||
Guard.NotNull(subject);
|
||||
Guard.NotNull(clip);
|
||||
|
||||
PCPolygon s = PolygonClipperFactory.FromSimpleClosedPaths(subject.Flatten());
|
||||
PCPolygon c = PolygonClipperFactory.FromClosedPaths(clip);
|
||||
|
||||
PCPolygon result = operation switch
|
||||
{
|
||||
BooleanOperation.Xor => PolygonClipperAction.Xor(s, c),
|
||||
BooleanOperation.Difference => PolygonClipperAction.Difference(s, c),
|
||||
BooleanOperation.Union => PolygonClipperAction.Union(s, c),
|
||||
_ => PolygonClipperAction.Intersection(s, c),
|
||||
};
|
||||
|
||||
IPath[] shapes = new IPath[result.Count];
|
||||
|
||||
int index = 0;
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
shapes[index++] = new Polygon(CreateContourPoints(result, i));
|
||||
}
|
||||
|
||||
return new(shapes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PolygonClipper contour to ImageSharp points and normalizes winding for parent/child rings.
|
||||
/// </summary>
|
||||
/// <param name="polygon">The polygon containing the contour hierarchy.</param>
|
||||
/// <param name="contourIndex">The contour index to convert.</param>
|
||||
/// <returns>The converted point array.</returns>
|
||||
private static PointF[] CreateContourPoints(PCPolygon polygon, int contourIndex)
|
||||
{
|
||||
Contour contour = polygon[contourIndex];
|
||||
PointF[] points = new PointF[contour.Count];
|
||||
bool reverse = ShouldReverseForNonZeroWinding(polygon, contourIndex);
|
||||
|
||||
if (!reverse)
|
||||
{
|
||||
for (int i = 0; i < contour.Count; i++)
|
||||
{
|
||||
Vertex vertex = contour[i];
|
||||
points[i] = new PointF((float)vertex.X, (float)vertex.Y);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
for (int sourceIndex = contour.Count - 1, targetIndex = 0; sourceIndex >= 0; sourceIndex--, targetIndex++)
|
||||
{
|
||||
Vertex vertex = contour[sourceIndex];
|
||||
points[targetIndex] = new PointF((float)vertex.X, (float)vertex.Y);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures child contours (holes/islands) use opposite winding to their direct parent.
|
||||
/// This keeps clipped output deterministic when consumed with the NonZero fill rule.
|
||||
/// </summary>
|
||||
/// <param name="polygon">The polygon containing contour hierarchy information.</param>
|
||||
/// <param name="contourIndex">The contour index to inspect.</param>
|
||||
/// <returns><see langword="true"/> when the contour should be reversed.</returns>
|
||||
private static bool ShouldReverseForNonZeroWinding(PCPolygon polygon, int contourIndex)
|
||||
{
|
||||
Contour contour = polygon[contourIndex];
|
||||
if (contour.ParentIndex is not int parentIndex || (uint)parentIndex >= (uint)polygon.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Contour parentContour = polygon[parentIndex];
|
||||
return contour.IsCounterClockwise() == parentContour.IsCounterClockwise();
|
||||
}
|
||||
}
|
||||
}
|
||||
81
ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs
Normal file
81
ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.PolygonClipper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PCPolygon = SixLabors.PolygonClipper.Polygon;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.PolygonGeometry {
|
||||
/// <summary>
|
||||
/// Builders for <see cref="PCPolygon"/> from ImageSharp paths.
|
||||
/// Converts ImageSharp paths to the format required by PolygonClipper.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// PolygonClipper computes parent-child relationships, depth, and orientation during its
|
||||
/// sweep line algorithm, so we only need to provide contours with vertices.
|
||||
/// </remarks>
|
||||
internal static class PolygonClipperFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a polygon from multiple paths.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths to convert.</param>
|
||||
/// <returns>A <see cref="PCPolygon"/> containing all flattened paths as contours.</returns>
|
||||
public static PCPolygon FromClosedPaths(IEnumerable<IPath> paths)
|
||||
{
|
||||
PCPolygon polygon = [];
|
||||
|
||||
foreach (IPath path in paths)
|
||||
{
|
||||
polygon = FromSimpleClosedPaths(path.Flatten(), polygon);
|
||||
}
|
||||
|
||||
return polygon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts closed simple paths to PolygonClipper contours.
|
||||
/// </summary>
|
||||
/// <param name="paths">Closed simple paths.</param>
|
||||
/// <param name="polygon">Optional existing polygon to populate.</param>
|
||||
/// <returns>The constructed <see cref="PCPolygon"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method simply converts ImageSharp paths to PolygonClipper contours by copying vertices.
|
||||
/// PolygonClipper's sweep line algorithm will determine parent-child relationships, depth,
|
||||
/// and proper orientation during clipping operations. We only need to ensure paths are
|
||||
/// closed and have sufficient vertices.
|
||||
/// </remarks>
|
||||
public static PCPolygon FromSimpleClosedPaths(IEnumerable<ISimplePath> paths, PCPolygon? polygon = null)
|
||||
{
|
||||
polygon ??= [];
|
||||
|
||||
foreach (ISimplePath p in paths)
|
||||
{
|
||||
if (!p.IsClosed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ReadOnlySpan<PointF> points = p.Points.Span;
|
||||
if (points.Length < 3)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Contour contour = [];
|
||||
|
||||
// Copy all vertices
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
{
|
||||
contour.Add(new Vertex(points[i].X, points[i].Y));
|
||||
}
|
||||
|
||||
// Add the contour - PolygonClipper will determine parent/depth/orientation during sweep
|
||||
polygon.Add(contour);
|
||||
}
|
||||
|
||||
return polygon;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs
Normal file
106
ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.PolygonClipper;
|
||||
using System;
|
||||
using PCPolygon = SixLabors.PolygonClipper.Polygon;
|
||||
using StrokeOptions = SixLabors.ImageSharp.Drawing.Processing.StrokeOptions;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.PolygonGeometry {
|
||||
/// <summary>
|
||||
/// Generates stroked and merged shapes using polygon stroking and boolean clipping.
|
||||
/// </summary>
|
||||
internal static class StrokedShapeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Strokes a path and returns a merged outline from its flattened segments.
|
||||
/// </summary>
|
||||
/// <param name="path">The source path. It is flattened using the current flattening settings.</param>
|
||||
/// <param name="width">The stroke width in the caller's coordinate space.</param>
|
||||
/// <param name="options">The stroke geometry options.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="ComplexPolygon"/> representing the stroked outline after boolean merge.
|
||||
/// </returns>
|
||||
public static ComplexPolygon GenerateStrokedShapes(IPath path, float width, StrokeOptions options)
|
||||
{
|
||||
// 1) Stroke the input path as open or closed.
|
||||
PCPolygon rings = [];
|
||||
|
||||
foreach (ISimplePath sp in path.Flatten())
|
||||
{
|
||||
ReadOnlySpan<PointF> span = sp.Points.Span;
|
||||
|
||||
if (span.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Contour ring = new(span.Length);
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
PointF p = span[i];
|
||||
ring.Add(new Vertex(p.X, p.Y));
|
||||
}
|
||||
|
||||
if (sp.IsClosed)
|
||||
{
|
||||
ring.Add(ring[0]);
|
||||
}
|
||||
|
||||
rings.Add(ring);
|
||||
}
|
||||
|
||||
int count = rings.Count;
|
||||
if (count == 0)
|
||||
{
|
||||
return new([]);
|
||||
}
|
||||
|
||||
PCPolygon result = PolygonStroker.Stroke(rings, width, CreateStrokeOptions(options));
|
||||
|
||||
IPath[] shapes = new IPath[result.Count];
|
||||
int index = 0;
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
Contour contour = result[i];
|
||||
PointF[] points = new PointF[contour.Count];
|
||||
|
||||
for (int j = 0; j < contour.Count; j++)
|
||||
{
|
||||
Vertex vertex = contour[j];
|
||||
points[j] = new PointF((float)vertex.X, (float)vertex.Y);
|
||||
}
|
||||
|
||||
shapes[index++] = new Polygon(points);
|
||||
}
|
||||
|
||||
return new(shapes);
|
||||
}
|
||||
|
||||
private static PolygonClipper.StrokeOptions CreateStrokeOptions(StrokeOptions options)
|
||||
{
|
||||
PolygonClipper.StrokeOptions o = new()
|
||||
{
|
||||
ArcDetailScale = options.ArcDetailScale,
|
||||
MiterLimit = options.MiterLimit,
|
||||
LineJoin = options.LineJoin switch
|
||||
{
|
||||
LineJoin.MiterRound => PolygonClipper.LineJoin.MiterRound,
|
||||
LineJoin.Bevel => PolygonClipper.LineJoin.Bevel,
|
||||
LineJoin.Round => PolygonClipper.LineJoin.Round,
|
||||
LineJoin.MiterRevert => PolygonClipper.LineJoin.MiterRevert,
|
||||
_ => PolygonClipper.LineJoin.Miter,
|
||||
},
|
||||
|
||||
LineCap = options.LineCap switch
|
||||
{
|
||||
LineCap.Round => PolygonClipper.LineCap.Round,
|
||||
LineCap.Square => PolygonClipper.LineCap.Square,
|
||||
_ => PolygonClipper.LineCap.Butt,
|
||||
}
|
||||
};
|
||||
|
||||
return o;
|
||||
}
|
||||
}
|
||||
}
|
||||
176
ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs
Normal file
176
ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs
Normal file
@ -0,0 +1,176 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Processor barrier recorded in a drawing backend timeline.
|
||||
/// </summary>
|
||||
internal sealed class ApplyBarrier
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplyBarrier"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The closed path defining the processed region.</param>
|
||||
/// <param name="options">The drawing options captured when the barrier was recorded.</param>
|
||||
/// <param name="clipPaths">The active clip paths captured when the barrier was recorded.</param>
|
||||
/// <param name="canvasBounds">The canvas-local bounds captured when the barrier was recorded.</param>
|
||||
/// <param name="targetBounds">The absolute target bounds captured when the barrier was recorded.</param>
|
||||
/// <param name="destinationOffset">The absolute destination offset captured when the barrier was recorded.</param>
|
||||
/// <param name="isInsideLayer">Indicates whether the barrier was recorded inside a layer.</param>
|
||||
/// <param name="operation">The processor operation to run against the replay-time snapshot.</param>
|
||||
internal ApplyBarrier(
|
||||
IPath path,
|
||||
DrawingOptions options,
|
||||
IReadOnlyList<IPath> clipPaths,
|
||||
Rectangle canvasBounds,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
bool isInsideLayer,
|
||||
Action<IImageProcessingContext> operation)
|
||||
{
|
||||
this.Path = path;
|
||||
this.Options = options;
|
||||
this.ClipPaths = clipPaths;
|
||||
this.CanvasBounds = canvasBounds;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
this.Operation = operation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the closed path defining the processed region.
|
||||
/// </summary>
|
||||
public IPath Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public DrawingOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active clip paths captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath> ClipPaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the canvas-local bounds captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public Rectangle CanvasBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute target bounds captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the barrier was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the processor operation to run against the replay-time snapshot.
|
||||
/// </summary>
|
||||
public Action<IImageProcessingContext> Operation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the transient image-brush draw command that writes this barrier's processed snapshot back to the target.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="backend">The backend used to read the replay-time target pixels.</param>
|
||||
/// <param name="target">The target frame.</param>
|
||||
/// <param name="ownedResource">The image resource that must stay alive while the returned command batch is rendered.</param>
|
||||
/// <returns>The transient write-back command batch, or <see langword="null"/> when the barrier has no target coverage.</returns>
|
||||
public DrawingCommandBatch? CreateWriteBackBatch<TPixel>(
|
||||
Configuration configuration,
|
||||
IDrawingBackend backend,
|
||||
ICanvasFrame<TPixel> target,
|
||||
out IDisposable? ownedResource)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
RectangleF rawBounds = RectangleF.Transform(this.Path.Bounds, this.Options.Transform);
|
||||
Rectangle sourceRect = ToConservativeBounds(rawBounds);
|
||||
sourceRect = Rectangle.Intersect(this.CanvasBounds, sourceRect);
|
||||
|
||||
if (sourceRect.Width <= 0 || sourceRect.Height <= 0)
|
||||
{
|
||||
ownedResource = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
Image<TPixel> sourceImage = new(configuration, sourceRect.Width, sourceRect.Height);
|
||||
try
|
||||
{
|
||||
backend.ReadRegion(
|
||||
configuration,
|
||||
target,
|
||||
sourceRect,
|
||||
sourceImage.Frames.RootFrame.PixelBuffer.GetRegion());
|
||||
|
||||
sourceImage.Mutate(this.Operation);
|
||||
|
||||
Point brushOffset = new(
|
||||
sourceRect.X - (int)MathF.Floor(rawBounds.Left),
|
||||
sourceRect.Y - (int)MathF.Floor(rawBounds.Top));
|
||||
|
||||
ImageBrush<TPixel> brush = new(sourceImage, sourceImage.Bounds, brushOffset);
|
||||
GraphicsOptions graphicsOptions = this.Options.GraphicsOptions;
|
||||
RasterizationMode rasterizationMode = graphicsOptions.Antialias
|
||||
? RasterizationMode.Antialiased
|
||||
: RasterizationMode.Aliased;
|
||||
|
||||
RectangleF pathBounds = this.Path.Bounds;
|
||||
Rectangle interest = Rectangle.FromLTRB(
|
||||
(int)MathF.Floor(pathBounds.Left),
|
||||
(int)MathF.Floor(pathBounds.Top),
|
||||
(int)MathF.Ceiling(pathBounds.Right),
|
||||
(int)MathF.Ceiling(pathBounds.Bottom));
|
||||
|
||||
RasterizerOptions rasterizerOptions = new(
|
||||
interest,
|
||||
this.Options.ShapeOptions.IntersectionRule,
|
||||
rasterizationMode,
|
||||
RasterizerSamplingOrigin.PixelBoundary,
|
||||
graphicsOptions.AntialiasThreshold);
|
||||
|
||||
CompositionCommand command = CompositionCommand.Create(
|
||||
this.Path,
|
||||
brush,
|
||||
this.Options,
|
||||
in rasterizerOptions,
|
||||
this.TargetBounds,
|
||||
this.DestinationOffset,
|
||||
this.ClipPaths,
|
||||
this.IsInsideLayer);
|
||||
|
||||
ownedResource = sourceImage;
|
||||
CompositionSceneCommand[] commands = [new PathCompositionSceneCommand(command)];
|
||||
return new DrawingCommandBatch(commands, hasLayers: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
sourceImage.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static Rectangle ToConservativeBounds(RectangleF bounds)
|
||||
=> Rectangle.FromLTRB(
|
||||
(int)MathF.Floor(bounds.Left),
|
||||
(int)MathF.Floor(bounds.Top),
|
||||
(int)MathF.Ceiling(bounds.Right),
|
||||
(int)MathF.Ceiling(bounds.Bottom));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Frame adapter that exposes a clipped subregion of another frame.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
internal sealed class CanvasRegionFrame<TPixel> : ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly ICanvasFrame<TPixel> parent;
|
||||
private readonly Rectangle region;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CanvasRegionFrame{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent frame that owns the target pixels.</param>
|
||||
/// <param name="region">The child region in parent-local coordinates.</param>
|
||||
public CanvasRegionFrame(ICanvasFrame<TPixel> parent, Rectangle region)
|
||||
{
|
||||
Guard.NotNull(parent, nameof(parent));
|
||||
Guard.MustBeGreaterThanOrEqualTo(region.Width, 0, nameof(region));
|
||||
Guard.MustBeGreaterThanOrEqualTo(region.Height, 0, nameof(region));
|
||||
|
||||
this.parent = parent;
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rectangle Bounds => new(
|
||||
this.parent.Bounds.X + this.region.X,
|
||||
this.parent.Bounds.Y + this.region.Y,
|
||||
this.region.Width,
|
||||
this.region.Height);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
if (!this.parent.TryGetCpuRegion(out Buffer2DRegion<TPixel> parentRegion))
|
||||
{
|
||||
region = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
region = parentRegion.GetSubRegion(this.region);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface)
|
||||
=> this.parent.TryGetNativeSurface(out surface);
|
||||
}
|
||||
}
|
||||
215
ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs
Normal file
215
ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs
Normal file
@ -0,0 +1,215 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Identifies the flush-time role carried by a <see cref="CompositionCommand"/>.
|
||||
/// </summary>
|
||||
public enum CompositionCommandKind : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// A fill-path command.
|
||||
/// </summary>
|
||||
FillLayer = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Starts an isolated compositing layer.
|
||||
/// </summary>
|
||||
BeginLayer = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Ends the most recently opened layer.
|
||||
/// </summary>
|
||||
EndLayer = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One normalized fill-path or layer-based composition command queued for backend execution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This type carries fill-path commands plus inline layer boundaries.
|
||||
/// </remarks>
|
||||
public readonly struct CompositionCommand
|
||||
{
|
||||
private readonly IPath? sourcePath;
|
||||
private readonly Brush? brush;
|
||||
private readonly DrawingOptions? drawingOptions;
|
||||
private readonly GraphicsOptions? layerGraphicsOptions;
|
||||
private readonly IReadOnlyList<IPath>? clipPaths;
|
||||
|
||||
private CompositionCommand(
|
||||
CompositionCommandKind kind,
|
||||
IPath? sourcePath,
|
||||
Brush? brush,
|
||||
DrawingOptions? drawingOptions,
|
||||
GraphicsOptions? layerGraphicsOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Rectangle layerBounds,
|
||||
Point destinationOffset,
|
||||
IReadOnlyList<IPath>? clipPaths,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
this.Kind = kind;
|
||||
this.sourcePath = sourcePath;
|
||||
this.brush = brush;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.layerGraphicsOptions = layerGraphicsOptions;
|
||||
this.RasterizerOptions = rasterizerOptions;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.LayerBounds = layerBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.clipPaths = clipPaths;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command kind.
|
||||
/// </summary>
|
||||
public CompositionCommandKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the logical target for this command.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the layer opened by this command.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only meaningful for <see cref="CompositionCommandKind.BeginLayer"/> and
|
||||
/// <see cref="CompositionCommandKind.EndLayer"/>.
|
||||
/// </remarks>
|
||||
public Rectangle LayerBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used during composition.
|
||||
/// </summary>
|
||||
public Brush Brush => this.brush ?? throw new InvalidOperationException("Layer commands do not carry a brush.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options carried by the command.
|
||||
/// </summary>
|
||||
public DrawingOptions DrawingOptions => this.drawingOptions ?? throw new InvalidOperationException("Layer commands do not carry drawing options.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets graphics options used for composition or layer compositing.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions => this.drawingOptions?.GraphicsOptions ?? this.layerGraphicsOptions!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets rasterizer options used to generate coverage.
|
||||
/// </summary>
|
||||
public RasterizerOptions RasterizerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset where the local coverage should be composited.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source path carried by the command.
|
||||
/// </summary>
|
||||
public IPath SourcePath => this.sourcePath ?? throw new InvalidOperationException("Layer commands do not carry path geometry.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions?.Transform ?? Matrix4x4.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the clip paths carried by the command.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath>? ClipPaths => this.clipPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shape options carried by the command.
|
||||
/// </summary>
|
||||
public ShapeOptions ShapeOptions => this.drawingOptions?.ShapeOptions ?? throw new InvalidOperationException("Layer commands do not carry shape options.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fill-path composition command.
|
||||
/// </summary>
|
||||
/// <param name="path">Path in target-local coordinates.</param>
|
||||
/// <param name="brush">Brush used during composition.</param>
|
||||
/// <param name="drawingOptions">Drawing options (graphics, shape, transform) used during composition.</param>
|
||||
/// <param name="rasterizerOptions">Rasterizer options used to generate coverage.</param>
|
||||
/// <param name="targetBounds">The absolute bounds of the logical target for this command.</param>
|
||||
/// <param name="destinationOffset">Absolute destination offset where coverage is composited.</param>
|
||||
/// <param name="clipPaths">Optional clip paths supplied with the command.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
/// <returns>The composition command.</returns>
|
||||
public static CompositionCommand Create(
|
||||
IPath path,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
IReadOnlyList<IPath>? clipPaths,
|
||||
bool isInsideLayer)
|
||||
=> new(
|
||||
CompositionCommandKind.FillLayer,
|
||||
path,
|
||||
brush,
|
||||
drawingOptions,
|
||||
null,
|
||||
in rasterizerOptions,
|
||||
targetBounds,
|
||||
default,
|
||||
destinationOffset,
|
||||
clipPaths,
|
||||
isInsideLayer);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a begin-layer composition command. <see cref="IsInsideLayer"/> is false on the
|
||||
/// BeginLayer marker itself; the flag is only meaningful for fills/strokes that follow it.
|
||||
/// </summary>
|
||||
/// <param name="layerBounds">The absolute bounds of the layer.</param>
|
||||
/// <param name="graphicsOptions">The compositing options used when the layer closes.</param>
|
||||
/// <returns>The begin-layer command.</returns>
|
||||
public static CompositionCommand CreateBeginLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions)
|
||||
=> new(
|
||||
CompositionCommandKind.BeginLayer,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
graphicsOptions,
|
||||
default,
|
||||
layerBounds,
|
||||
layerBounds,
|
||||
default,
|
||||
null,
|
||||
false);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an end-layer composition command. <see cref="IsInsideLayer"/> is false on the
|
||||
/// EndLayer marker itself; the flag is only meaningful for fills/strokes that preceded it.
|
||||
/// </summary>
|
||||
/// <param name="layerBounds">The absolute bounds of the layer being closed.</param>
|
||||
/// <param name="graphicsOptions">The compositing options used by the layer.</param>
|
||||
/// <returns>The end-layer command.</returns>
|
||||
public static CompositionCommand CreateEndLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions)
|
||||
=> new(
|
||||
CompositionCommandKind.EndLayer,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
graphicsOptions,
|
||||
default,
|
||||
layerBounds,
|
||||
layerBounds,
|
||||
default,
|
||||
null,
|
||||
false);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
#pragma warning disable SA1649 // Scene command types are grouped together in one file.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Visitor contract for one flush-scoped composition scene command.
|
||||
/// </summary>
|
||||
public interface ICompositionSceneCommandVisitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Visits one fill-path or layer-based composition command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(PathCompositionSceneCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Visits one stroked path command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(StrokePathCompositionSceneCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Visits one explicit stroked line-segment command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(LineSegmentCompositionSceneCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Visits one explicit stroked polyline command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(PolylineCompositionSceneCommand command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base type for one draw-order command in a flush-scoped scene stream.
|
||||
/// </summary>
|
||||
public abstract class CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatches the command to a visitor without a per-item kind switch at the call site.
|
||||
/// </summary>
|
||||
/// <param name="visitor">The visitor receiving the command.</param>
|
||||
public abstract void Accept(ICompositionSceneCommandVisitor visitor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for fill-path and layer-based composition commands.
|
||||
/// </summary>
|
||||
public sealed class PathCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped composition command.</param>
|
||||
public PathCompositionSceneCommand(in CompositionCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped composition command.
|
||||
/// </summary>
|
||||
public CompositionCommand Command { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for stroked path commands.
|
||||
/// </summary>
|
||||
public sealed class StrokePathCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokePathCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped stroke path command.</param>
|
||||
public StrokePathCompositionSceneCommand(in StrokePathCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped stroke path command.
|
||||
/// </summary>
|
||||
public StrokePathCommand Command { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for explicit stroked line-segment commands.
|
||||
/// </summary>
|
||||
public sealed class LineSegmentCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineSegmentCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped stroke line-segment command.</param>
|
||||
public LineSegmentCompositionSceneCommand(in StrokeLineSegmentCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped stroke line-segment command.
|
||||
/// </summary>
|
||||
public StrokeLineSegmentCommand Command { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for explicit stroked polyline commands.
|
||||
/// </summary>
|
||||
public sealed class PolylineCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PolylineCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped stroke polyline command.</param>
|
||||
public PolylineCompositionSceneCommand(in StrokePolylineCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped stroke polyline command.
|
||||
/// </summary>
|
||||
public StrokePolylineCommand Command { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,373 @@
|
||||
# DefaultDrawingBackend
|
||||
|
||||
`DefaultDrawingBackend` is the CPU execution backend for ImageSharp.Drawing. It creates retained CPU scenes from prepared drawing command batches, executes those scenes with reusable scratch, and writes the result into a CPU destination buffer.
|
||||
|
||||
This document explains the backend as a system rather than as a list of methods. The goal is to help a newcomer understand:
|
||||
|
||||
- where the CPU backend fits in the canvas/backend selection model
|
||||
- what problem the CPU backend is solving
|
||||
- why the backend is organized around a retained row-oriented execution plan
|
||||
- what `FlushScene` means in this architecture
|
||||
- how rasterization, brush application, and layer composition fit together
|
||||
|
||||
## Where The CPU Backend Fits
|
||||
|
||||
`DefaultDrawingBackend` is the standard CPU execution path behind `DrawingCanvas`.
|
||||
|
||||
The canvas architecture reaches this backend in two common ways:
|
||||
|
||||
- ordinary typed canvas construction resolves `IDrawingBackend` from `Configuration`
|
||||
- specialized infrastructure can construct a canvas with an explicit backend instance
|
||||
|
||||
The CPU path usually uses the first route. The WebGPU helpers use the second route when they need a canvas that targets a native surface through `WebGPUDrawingBackend`.
|
||||
|
||||
That means the CPU backend is one backend implementation within the shared canvas architecture, not a separate public drawing model. It executes against any frame that exposes a writable CPU region, whether that frame is pure memory or a hybrid frame that also carries a native surface.
|
||||
|
||||
## The Main Problem
|
||||
|
||||
By the time work reaches `DefaultDrawingBackend`, the public drawing API has already been normalized into prepared commands. That is helpful, but it does not make CPU execution trivial.
|
||||
|
||||
The backend still has to solve a hard scheduling problem.
|
||||
|
||||
It needs to answer questions such as:
|
||||
|
||||
- which destination rows each command touches
|
||||
- how to preserve draw order while running work in parallel
|
||||
- how to avoid re-deriving geometry information in the hot loop
|
||||
- where temporary memory should live and when it should be reused
|
||||
|
||||
If the CPU backend executed commands directly from the incoming scene, each worker would repeatedly rediscover which rows matter, which parts of the geometry matter in those rows, and how much scratch is needed. That would push expensive planning work into the hottest part of the pipeline.
|
||||
|
||||
So the backend takes a different approach:
|
||||
|
||||
it turns the whole command batch into a row-oriented execution plan first, then executes that plan.
|
||||
|
||||
That decision explains most of the backend architecture.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
The CPU backend is a flush executor, not a command-at-a-time painter.
|
||||
|
||||
Its central idea is:
|
||||
|
||||
> convert a command batch into row-local raster work once, then execute rows directly with reusable worker-local scratch
|
||||
|
||||
That is why the backend is built around `FlushScene`.
|
||||
|
||||
`FlushScene` is a retained execution plan. In non-retained rendering it is short-lived and disposed after one replay entry; in retained rendering it can live with the returned `DefaultDrawingBackendScene`. Its job is to take a prepared command stream and reorganize it into a form that is cheap for the row executor to consume.
|
||||
|
||||
If that idea is clear, most of the important types fall into place.
|
||||
|
||||
## The Most Important Terms
|
||||
|
||||
### Backend
|
||||
|
||||
`DefaultDrawingBackend` is the top-level CPU executor. It owns backend policy and orchestration:
|
||||
|
||||
- acquiring a writable CPU destination
|
||||
- creating the retained execution plan
|
||||
- executing that plan
|
||||
- handling CPU layer composition
|
||||
|
||||
It does not own every detail of geometry planning or scan conversion.
|
||||
|
||||
It also does not own backend selection. By the time `CreateScene(...)` or `RenderScene(...)` is called, the typed canvas implementation has already chosen the backend instance that will receive the prepared work.
|
||||
|
||||
### Scene
|
||||
|
||||
In the canvas architecture, the backend receives a `DrawingCommandBatch`. That batch already contains prepared commands and explicit layer boundaries for one contiguous command range.
|
||||
|
||||
For the CPU backend, that incoming batch is the starting point, not the final execution form.
|
||||
|
||||
### Flush Scene
|
||||
|
||||
`FlushScene` is the most important supporting type in the CPU backend.
|
||||
|
||||
In this codebase, `FlushScene` means:
|
||||
|
||||
"the retained, row-oriented execution plan for one CPU command batch"
|
||||
|
||||
It owns the retained information needed to make execution cheap:
|
||||
|
||||
- the visible prepared commands
|
||||
- retained rasterizable geometry
|
||||
- row membership
|
||||
- row-local execution items
|
||||
- scratch size requirements for the flush
|
||||
|
||||
### Rasterizer
|
||||
|
||||
`DefaultRasterizer` is the geometry-to-coverage engine.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- fixed-point scan conversion
|
||||
- fill-rule handling
|
||||
- coverage accumulation
|
||||
- emitting row coverage spans
|
||||
|
||||
It is not responsible for deciding which commands should run in which rows, and it does not write final pixels directly.
|
||||
|
||||
### Brush Renderer
|
||||
|
||||
`BrushRenderer<TPixel>` is the coverage-to-color engine for one prepared drawing command.
|
||||
|
||||
It receives:
|
||||
|
||||
- a destination row slice
|
||||
- coverage data
|
||||
- destination position
|
||||
- reusable workspace
|
||||
|
||||
and updates pixels accordingly.
|
||||
|
||||
The important separation is:
|
||||
|
||||
- the rasterizer decides coverage
|
||||
- the brush renderer decides color
|
||||
- the backend executor binds the two together
|
||||
|
||||
### Worker State
|
||||
|
||||
`WorkerState<TPixel>` is the reusable per-worker execution state.
|
||||
|
||||
It owns worker-local scratch such as:
|
||||
|
||||
- raster scratch
|
||||
- brush workspace
|
||||
- the coverage row handler state
|
||||
|
||||
This is how the backend avoids allocating fresh buffers for every row item during the hot parallel pass.
|
||||
|
||||
## The Big Picture Flow
|
||||
|
||||
The easiest way to understand the backend is to follow one command batch from scene creation to execution.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[DrawingCanvas disposal replay] --> B[DefaultDrawingBackend.CreateScene]
|
||||
B --> C[FlushScene.Create]
|
||||
C --> D[Prepare visible items]
|
||||
D --> E[Build row-local execution plan]
|
||||
E --> F[DefaultDrawingBackend.RenderScene]
|
||||
F --> G[Acquire CPU destination]
|
||||
G --> H[Execute rows in parallel]
|
||||
H --> I[DefaultRasterizer emits coverage]
|
||||
I --> J[BrushRenderer shades pixels]
|
||||
J --> K[Destination frame updated]
|
||||
```
|
||||
|
||||
There are three major stages in that flow:
|
||||
|
||||
1. build the retained execution plan
|
||||
2. establish the destination frame
|
||||
3. execute rows using that plan
|
||||
|
||||
## What `DefaultDrawingBackend` Owns
|
||||
|
||||
`DefaultDrawingBackend` is intentionally smaller than its supporting types. It owns orchestration, not every low-level detail.
|
||||
|
||||
Its responsibilities are:
|
||||
|
||||
- create a `FlushScene`
|
||||
- acquire a writable CPU region from the target frame
|
||||
- execute that scene
|
||||
- provide CPU layer composition services
|
||||
- manage frame usage for CPU-backed targets
|
||||
|
||||
The expensive work is delegated:
|
||||
|
||||
- `FlushScene` owns retained row planning
|
||||
- `DefaultRasterizer` owns scan conversion
|
||||
- `BrushRenderer<TPixel>` owns brush-specific shading
|
||||
|
||||
That split keeps each type focused on one class of problem.
|
||||
|
||||
The canvas layer above that split is also important:
|
||||
|
||||
- `DrawingCanvas` records public drawing intent
|
||||
- `DrawingCanvasBatcher<TPixel>` prepares commands and constructs `DrawingCommandBatch` values
|
||||
- `DefaultDrawingBackend` executes the retained scene on a CPU destination
|
||||
|
||||
## Building The Flush Scene
|
||||
|
||||
`FlushScene.Create(...)` turns the prepared command stream into an execution plan in several phases. Each phase changes the data into a form that is cheaper for the next phase to consume.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Prepared commands] --> B[Filter and compact visible work]
|
||||
B --> C[Create retained raster geometry]
|
||||
C --> D[Build row membership]
|
||||
D --> E[Build row-local execution items]
|
||||
E --> F[FlushScene]
|
||||
```
|
||||
|
||||
### 1. Filter and compact visible work
|
||||
|
||||
The scene builder begins from the incoming command stream and keeps only the work that is visible and relevant to the flush. The later phases should not pay repeatedly for invisible commands through sparse scans or conditional branching.
|
||||
|
||||
### 2. Create retained raster geometry
|
||||
|
||||
For each visible item, the builder decomposes the command's drawing matrix into an X/Y scale and the rotation-shear-translation-perspective residual, asks the path for its scale-baked `LinearGeometry` via `ToLinearGeometry(Vector2 scale)`, and hands both the geometry and the residual to `DefaultRasterizer` to create the retained rasterizable payload. Curve subdivision therefore happens once per (path, scale) pair — cached on the `IPath` — and any per-frame rotation or translation rides into the rasterizer as the residual without forcing the path to re-flatten.
|
||||
|
||||
This step matters because it moves expensive geometry preparation out of the hot row loop and out of every frame of workloads like text or panning that drift only in their residual.
|
||||
|
||||
### 3. Build row membership
|
||||
|
||||
Once retained geometry exists, the scene builder determines which scene rows each item touches. That produces row-local membership information while preserving original submission order within every row.
|
||||
|
||||
That detail is critical. Parallel execution is allowed, but draw order must remain deterministic within each row.
|
||||
|
||||
### 4. Build row-local execution items
|
||||
|
||||
The scene then materializes the payload that the row executor will visit. Each row item points into flush-owned retained storage and carries just enough metadata to reconstruct a cheap `RasterizableBand` view when execution reaches that row.
|
||||
|
||||
At that point the scene is execution-ready.
|
||||
|
||||
## Why The Backend Is Row-First
|
||||
|
||||
The CPU backend executes rows, not commands.
|
||||
|
||||
This is one of the most important architectural choices in the whole path.
|
||||
|
||||
Why it helps:
|
||||
|
||||
- each worker naturally touches localized destination memory
|
||||
- scratch can be reused across many row items
|
||||
- draw order is straightforward inside a row
|
||||
- geometry planning stays out of the hottest loop
|
||||
|
||||
A row-first executor fits the actual shape of CPU rendering much better than a command-first executor would.
|
||||
|
||||
## The Execution Pass
|
||||
|
||||
When `FlushScene.Execute(...)` runs, the backend prepares brush renderers and then executes scene rows in parallel.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Exec as FlushScene.Execute
|
||||
participant Worker as WorkerState
|
||||
participant Raster as DefaultRasterizer
|
||||
participant Brush as BrushRenderer
|
||||
|
||||
Exec->>Brush: create one renderer per visible item
|
||||
Exec->>Worker: start parallel row pass
|
||||
Worker->>Exec: enumerate row items in order
|
||||
Worker->>Raster: ExecuteRasterizableBand(...)
|
||||
Raster-->>Exec: coverage rows
|
||||
Exec->>Brush: Apply(...)
|
||||
```
|
||||
|
||||
There are two important ownership patterns in that pass:
|
||||
|
||||
- renderers are created once per visible item before the hot row loop
|
||||
- scratch and workspace are reused per worker during the row loop
|
||||
|
||||
That is one of the backend's main performance properties.
|
||||
|
||||
## How Rasterization and Shading Stay Separate
|
||||
|
||||
The rasterizer and the backend solve different problems.
|
||||
|
||||
`DefaultRasterizer` is responsible for geometry and coverage.
|
||||
|
||||
`DefaultDrawingBackend` and `FlushScene` are responsible for:
|
||||
|
||||
- which items execute
|
||||
- when they execute
|
||||
- where their coverage belongs in the destination
|
||||
- which brush renderer should consume that coverage
|
||||
|
||||
That separation is intentional. It lets the rasterizer stay geometry-focused while the backend handles composition and destination layout.
|
||||
|
||||
## Coverage Routing
|
||||
|
||||
The rasterizer does not write destination pixels directly. Instead it emits row coverage through a handler supplied by the backend.
|
||||
|
||||
The backend-side row handler:
|
||||
|
||||
- receives emitted coverage
|
||||
- maps band-local coordinates back into destination coordinates
|
||||
- slices the correct destination row
|
||||
- invokes the correct `BrushRenderer<TPixel>`
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Rasterizer coverage row] --> B[Row handler]
|
||||
B --> C[Map to destination slice]
|
||||
C --> D[BrushRenderer.Apply]
|
||||
D --> E[Pixels updated]
|
||||
```
|
||||
|
||||
This is why the brush renderer can stay target-unbound. It receives the destination row slice and coverage data at execution time rather than owning the destination frame itself.
|
||||
|
||||
## Layer Composition
|
||||
|
||||
CPU layer composition is a separate concern from path rasterization.
|
||||
|
||||
`ComposeLayer<TPixel>()` composites one CPU frame into another using `PixelBlender<TPixel>`. That path exists because compositing an already-rasterized layer is a different problem from scanning geometry into coverage.
|
||||
|
||||
Keeping those paths separate makes the backend easier to reason about.
|
||||
|
||||
## Frame And Memory Lifetime
|
||||
|
||||
The backend aligns ownership with the actual execution lifetime.
|
||||
|
||||
### Flush-owned
|
||||
|
||||
Owned by `FlushScene`:
|
||||
|
||||
- visible item arrays
|
||||
- row structures
|
||||
- retained raster data
|
||||
- start-cover storage
|
||||
|
||||
Disposed when the flush ends.
|
||||
|
||||
### Worker-owned
|
||||
|
||||
Owned by `WorkerState<TPixel>` during execution:
|
||||
|
||||
- raster scratch
|
||||
- brush workspace
|
||||
|
||||
Disposed when the worker completes.
|
||||
|
||||
### Item-owned
|
||||
|
||||
Created once per visible item during execution:
|
||||
|
||||
- `BrushRenderer<TPixel>`
|
||||
|
||||
Retained for the duration of the row pass and then released with the flush-owned scene item state.
|
||||
|
||||
That ownership model keeps allocation and disposal aligned with real work lifetime.
|
||||
|
||||
## Reading Guide
|
||||
|
||||
If you are new to this backend, read the code in this order:
|
||||
|
||||
1. `DrawingCanvas.cs`
|
||||
2. `DrawingCanvas{TPixel}.cs`
|
||||
3. `DrawingCanvasBatcher{TPixel}.cs`
|
||||
4. `DefaultDrawingBackend.cs`
|
||||
5. `FlushScene.cs`
|
||||
6. `FlushScene.RetainedTypes.cs`
|
||||
7. `DefaultDrawingBackend.Helpers.cs`
|
||||
8. `DefaultRasterizer.cs`
|
||||
|
||||
That order mirrors the runtime flow:
|
||||
|
||||
canvas and backend selection -> backend orchestration -> retained row planning -> row execution structures -> worker helpers -> scan conversion
|
||||
|
||||
## The Mental Model To Keep
|
||||
|
||||
The easiest way to keep this backend straight is to remember that it is not a command-at-a-time painter. It is a flush executor that converts visible commands into row-local retained raster work and then executes that work with reusable scratch.
|
||||
|
||||
If that model is clear, the major types fall into place:
|
||||
|
||||
- `DrawingCanvas` records intent, and the typed implementation selects the backend
|
||||
- `DefaultDrawingBackend` orchestrates
|
||||
- `FlushScene` plans
|
||||
- `DefaultRasterizer` converts geometry to coverage
|
||||
- `BrushRenderer<TPixel>` converts coverage to color
|
||||
458
ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md
Normal file
458
ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md
Normal file
@ -0,0 +1,458 @@
|
||||
# DefaultRasterizer
|
||||
|
||||
`DefaultRasterizer` is the CPU polygon scanner used by the retained fill path in ImageSharp.Drawing. Its job is narrow but central: take already-prepared geometry, convert that geometry into fixed-point edge contributions, and emit coverage rows that the CPU backend can turn into pixels.
|
||||
|
||||
This rasterizer is based on ideas and implementation techniques from the Blaze project:
|
||||
|
||||
- https://github.com/aurimasg/blaze
|
||||
|
||||
This document explains the rasterizer as a newcomer needs to understand it:
|
||||
|
||||
- where the rasterizer fits relative to `DrawingCanvas` and `DefaultDrawingBackend`
|
||||
- what problem the rasterizer is solving inside the CPU backend
|
||||
- why the rasterizer is split into retained geometry building and band execution
|
||||
- what retained geometry, bands, and coverage mean in this architecture
|
||||
- how scan conversion stays separate from brush shading and frame ownership
|
||||
|
||||
## Where The Rasterizer Fits
|
||||
|
||||
`DefaultRasterizer` sits below `DrawingCanvas`, the typed canvas implementation, and `DefaultDrawingBackend`.
|
||||
|
||||
The canvas records commands, the batcher prepares them into `DrawingCommandBatch` ranges, and `DefaultDrawingBackend` chooses the row-oriented execution plan for each retained CPU scene. `DefaultRasterizer` then handles the narrower geometry-to-coverage problem inside that CPU execution path.
|
||||
|
||||
That means the rasterizer does not select the backend, own the destination frame, or interpret the public drawing API directly. It receives already-prepared geometry through the CPU backend pipeline, and the backend later routes its coverage into whichever frame exposes the CPU region for the flush.
|
||||
|
||||
## The Main Problem
|
||||
|
||||
The CPU backend does not want to rediscover shape geometry every time it touches a destination row.
|
||||
|
||||
If row execution had to start from raw prepared paths every time, the backend would repeatedly need to:
|
||||
|
||||
- walk contours
|
||||
- split segments against row-band boundaries
|
||||
- compute left-of-band winding influence
|
||||
- rebuild scan-conversion state for the same shape over and over
|
||||
|
||||
That would push expensive geometry work into the hottest part of CPU rendering.
|
||||
|
||||
So the rasterizer solves a different problem:
|
||||
|
||||
it builds retained rasterizable geometry once, then executes compact band-local scanning work many times, cheaply.
|
||||
|
||||
That two-phase design is the core idea behind `DefaultRasterizer`.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
The rasterizer is a retained fixed-point polygon scanner.
|
||||
|
||||
Its central idea is:
|
||||
|
||||
> build band-local retained line data once, then execute fixed-point scan conversion from that retained data
|
||||
|
||||
This is why the rasterizer has two very different modes of work:
|
||||
|
||||
1. retained geometry building
|
||||
2. band execution
|
||||
|
||||
The first phase is a preparation phase. The second is the hot execution phase.
|
||||
|
||||
If that distinction is clear, the code becomes much easier to follow.
|
||||
|
||||
## The Most Important Terms
|
||||
|
||||
### Rasterizer
|
||||
|
||||
`DefaultRasterizer` is the geometry-to-coverage engine.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- converting prepared geometry into retained scan-conversion data
|
||||
- rasterizing retained band data with fixed-point arithmetic
|
||||
- emitting coverage rows
|
||||
|
||||
It is not responsible for:
|
||||
|
||||
- brush color generation
|
||||
- destination frame ownership
|
||||
- layer composition
|
||||
- deciding which scene items should execute
|
||||
|
||||
Those problems belong to the CPU backend and `FlushScene`.
|
||||
|
||||
### Retained Geometry
|
||||
|
||||
Retained geometry is the rasterizer's prepared execution payload.
|
||||
|
||||
In this codebase, retained geometry means:
|
||||
|
||||
"the fixed-point, band-local line data and start-cover seeds needed to rasterize one prepared shape later without revisiting its original contour data"
|
||||
|
||||
That retained form is stored in `RasterizableGeometry`.
|
||||
|
||||
### Band
|
||||
|
||||
A band is one small vertical slice of a shape's retained geometry.
|
||||
|
||||
The rasterizer does not keep one giant scene-wide edge table. It stores data in row bands so execution can stay local and bounded.
|
||||
|
||||
### Rasterizable Geometry
|
||||
|
||||
`RasterizableGeometry` is the retained representation of one prepared shape.
|
||||
|
||||
It stores:
|
||||
|
||||
- clipped local bounds
|
||||
- band-local metadata
|
||||
- retained line arrays
|
||||
- optional start-cover seeds for bands that need carry-in winding
|
||||
|
||||
This is the retained object that the CPU backend keeps in `FlushScene`.
|
||||
|
||||
### Rasterizable Band
|
||||
|
||||
A `RasterizableBand` is the execution-time view over one retained band of one retained shape.
|
||||
|
||||
It is the immediate input to `ExecuteRasterizableBand(...)`.
|
||||
|
||||
### Context
|
||||
|
||||
`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution.
|
||||
|
||||
It is a `ref struct` because it is tied directly to worker-owned scratch spans and should not escape the execution scope.
|
||||
|
||||
### Coverage
|
||||
|
||||
Coverage is the rasterizer's output.
|
||||
|
||||
The rasterizer does not decide final pixel colors. It decides how much geometric coverage each pixel receives. The backend later passes that coverage to a `BrushRenderer<TPixel>`, which decides how the destination pixels should be shaded.
|
||||
|
||||
## Pipeline Placement
|
||||
|
||||
The rasterizer sits in the middle of the CPU backend pipeline.
|
||||
|
||||
Upstream:
|
||||
|
||||
- `CompositionCommand` preparation produces prepared geometry
|
||||
- the typed canvas implementation and `DrawingCanvasBatcher<TPixel>` have already selected and called the CPU backend
|
||||
- `FlushScene` decides which items are visible and when they execute
|
||||
|
||||
Downstream:
|
||||
|
||||
- the rasterizer emits row coverage
|
||||
- `DefaultDrawingBackend` routes that coverage into `BrushRenderer<TPixel>.Apply(...)`
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Prepared geometry] --> B[DefaultRasterizer.CreateRasterizableGeometry]
|
||||
B --> C[RasterizableGeometry]
|
||||
C --> D[Build RasterizableBand view]
|
||||
D --> E[ExecuteRasterizableBand]
|
||||
E --> F[Coverage rows]
|
||||
F --> G[Brush renderer]
|
||||
```
|
||||
|
||||
That placement is important. The rasterizer is neither the public drawing model nor the final shading model. It is the geometry-to-coverage step between them.
|
||||
|
||||
## Why The Rasterizer Has Two Phases
|
||||
|
||||
The rasterizer separates:
|
||||
|
||||
1. building retained geometry
|
||||
2. executing retained geometry
|
||||
|
||||
### Phase 1: retained geometry building
|
||||
|
||||
`CreateRasterizableGeometry(...)` converts prepared geometry into a retained representation that is cheap to execute later.
|
||||
|
||||
This phase:
|
||||
|
||||
- walks prepared contours
|
||||
- converts coordinates into fixed-point
|
||||
- clips or splits segments as needed for band boundaries
|
||||
- records visible line pieces into retained line storage
|
||||
- records left-of-band winding influence into start-cover tables
|
||||
|
||||
The output is `RasterizableGeometry`.
|
||||
|
||||
### Phase 2: band execution
|
||||
|
||||
`ExecuteRasterizableBand(...)` is the hot execution entry point.
|
||||
|
||||
It does not revisit the original contour data. It receives a `RasterizableBand` view over retained data and performs the minimum work needed to emit coverage rows for that band.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Exec as ExecuteRasterizableBand
|
||||
participant Ctx as Context
|
||||
participant Emit as Coverage Row Handler
|
||||
|
||||
Exec->>Ctx: Reconfigure(...)
|
||||
Exec->>Ctx: SeedStartCovers(...)
|
||||
Exec->>Ctx: Rasterize retained lines
|
||||
Exec->>Ctx: EmitCoverageRows(...)
|
||||
Ctx-->>Emit: coverage rows
|
||||
Exec->>Ctx: ResetTouchedRows()
|
||||
```
|
||||
|
||||
That separation is one of the key reasons the retained fill path performs well. Expensive geometry work happens once; execution consumes compact band-local data.
|
||||
|
||||
## Fixed-Point Precision
|
||||
|
||||
The rasterizer works in 24.8 fixed-point coordinates.
|
||||
|
||||
That means:
|
||||
|
||||
- `1` pixel = `256` fixed-point units
|
||||
- `FixedShift = 8`
|
||||
- `FixedOne = 256`
|
||||
|
||||
This gives the scanner subpixel precision while keeping the hot execution path integer-based. Geometry may begin as floating-point path data, but once a retained line reaches the scan-conversion core it is treated as fixed-point state.
|
||||
|
||||
Coverage is converted back into normalized `float` values only at the emission boundary.
|
||||
|
||||
## Why Bands Exist
|
||||
|
||||
The rasterizer does not retain one monolithic edge table. It retains geometry in vertical row bands.
|
||||
|
||||
That matters because it keeps execution local and bounded.
|
||||
|
||||
When a segment crosses multiple bands, the linearizer splits it so each band receives only the portion it must scan. If a segment influences winding inside the visible band from the left side, that influence is folded into a start-cover seed rather than keeping an invisible off-screen line around forever.
|
||||
|
||||
This gives the backend several important properties:
|
||||
|
||||
- execution only touches the band it is currently composing
|
||||
- left-of-band winding can be precomputed
|
||||
- scratch requirements stay bounded
|
||||
- row-oriented execution consumes compact band-local payloads
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Contour segment] --> B{Touches one band?}
|
||||
B -- Yes --> C[Store visible line in that band]
|
||||
B -- No --> D[Split across band boundaries]
|
||||
D --> E[Store band-local visible pieces]
|
||||
D --> F[Accumulate start-cover seeds where needed]
|
||||
```
|
||||
|
||||
## Retained Geometry: What Gets Stored
|
||||
|
||||
`RasterizableGeometry` stores the retained data needed to rasterize a prepared shape later.
|
||||
|
||||
That includes:
|
||||
|
||||
- the local bounds of the prepared shape
|
||||
- band count and band-local metadata
|
||||
- retained line arrays for each band
|
||||
- optional start-cover arrays for bands that need carry-in winding
|
||||
|
||||
The retained line arrays use specialized storage formats such as:
|
||||
|
||||
- `LineArrayX16Y16`
|
||||
- `LineArrayX32Y16`
|
||||
|
||||
These are storage-oriented types. They exist to retain compact fixed-point line segments so execution does not need to revisit contour data.
|
||||
|
||||
## The Linearizer
|
||||
|
||||
The linearizer is the retained-geometry builder. It is generic over line-array storage, but the conceptual work is the same across variants.
|
||||
|
||||
Its responsibilities are:
|
||||
|
||||
- traverse prepared contours
|
||||
- apply the residual transform per-point as contours are read
|
||||
- clip work to retained bounds
|
||||
- convert coordinates into fixed-point
|
||||
- decide whether a segment is contained or must be split
|
||||
- store visible line pieces
|
||||
- accumulate start covers for left-of-band influence
|
||||
|
||||
For a newcomer, the most important thing to understand is that the linearizer is not the hot coverage emitter. It is the preparation step that turns arbitrary contour geometry into a stable retained scanning payload.
|
||||
|
||||
### Residual transform application
|
||||
|
||||
The prepared `LinearGeometry` passed to `CreateRasterizableGeometry(...)` carries scale-baked points — the effective X/Y scale of the drawing matrix has already been absorbed into the flattened contour, so curve subdivision happens at device-scale precision. The remaining rotation, shear, translation, and perspective is handed to the rasterizer as a separate `Matrix4x4 residual`, which the linearizer applies per-point where the contour is read: at segment emission time in `ProcessContained` / `ProcessUncontained` for fills, and at bounds / closure / contour-segment construction sites in the stroke linearizer.
|
||||
|
||||
This split keeps the scale-baked geometry cacheable across frames (text and panning workloads reuse the same bake at a fixed zoom) while letting per-frame rotation or translation ride through the rasterizer without re-subdividing curves.
|
||||
|
||||
### Contained lines
|
||||
|
||||
A contained line is one whose fixed-point endpoints already fit the assumptions of the current retained band representation. Those lines can be pushed directly into retained storage after the required fixed-point and band-boundary handling.
|
||||
|
||||
### Split lines
|
||||
|
||||
When a line crosses band boundaries, the linearizer splits it so each band receives only the contribution it needs to scan.
|
||||
|
||||
### Start-cover seeding
|
||||
|
||||
When a line contributes winding inside the visible band but lies partially to the left of the visible X range, the retained geometry stores that influence in a start-cover array instead of retaining an off-screen line.
|
||||
|
||||
This is one of the most important ideas in the retained design:
|
||||
|
||||
- visible geometry becomes retained lines
|
||||
- invisible left-of-band winding becomes retained start-cover seeds
|
||||
|
||||
## The Execution Context
|
||||
|
||||
`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution.
|
||||
|
||||
It owns per-band mutable state such as:
|
||||
|
||||
- `bitVectors`
|
||||
- `coverArea`
|
||||
- `startCover`
|
||||
- `rowMinTouchedColumn`
|
||||
- `rowMaxTouchedColumn`
|
||||
- `rowHasBits`
|
||||
- `rowTouched`
|
||||
- `touchedRows`
|
||||
|
||||
This state is reused across bands by reconfiguration, not by reallocation.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[WorkerScratch] --> B[Context]
|
||||
B --> C[Rasterize retained lines]
|
||||
C --> D[Mutate coverArea and bit vectors]
|
||||
D --> E[Emit coverage rows]
|
||||
E --> F[Reset touched rows]
|
||||
```
|
||||
|
||||
The `Context` bridges retained geometry and emitted coverage.
|
||||
|
||||
## How Coverage Accumulation Works
|
||||
|
||||
The rasterizer uses the classic area-and-cover formulation.
|
||||
|
||||
When a fixed-point line is rasterized, it is broken into cell contributions. Those contributions eventually reach `AddCell(...)`, which updates:
|
||||
|
||||
- delta cover
|
||||
- delta area
|
||||
|
||||
Rows also track sparse touched-column information through bit vectors, so the emitter can avoid scanning the full width of empty rows.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Rasterize fixed-point line] --> B[Decompose into touched cells]
|
||||
B --> C["AddCell(row, column, deltaCover, deltaArea)"]
|
||||
C --> D[Update coverArea]
|
||||
C --> E[Mark bitVectors]
|
||||
C --> F[Track touched rows and bounds]
|
||||
C --> G{column < 0?}
|
||||
G -- Yes --> H[Fold into startCover]
|
||||
G -- No --> I[Keep visible cell contribution]
|
||||
```
|
||||
|
||||
This is why the rasterizer can honor fill rules later. It accumulates signed contributions first and applies the fill rule during coverage emission.
|
||||
|
||||
## Coverage Emission
|
||||
|
||||
`EmitCoverageRows(...)` converts the accumulated fixed-point state into row spans.
|
||||
|
||||
For each touched row, the emitter:
|
||||
|
||||
1. starts from the seeded `startCover`
|
||||
2. walks the row's touched columns using the bit vectors
|
||||
3. updates the running cover from `deltaCover`
|
||||
4. combines running cover and `deltaArea` into signed area
|
||||
5. converts signed area into normalized coverage using the selected fill rule
|
||||
6. coalesces equal-coverage spans
|
||||
7. writes only non-zero spans into the reusable scanline buffer
|
||||
8. invokes the row callback
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Touched row] --> B[Walk set bits]
|
||||
B --> C[Reconstruct cover and area]
|
||||
C --> D[Apply fill rule]
|
||||
D --> E[Coalesce equal coverage]
|
||||
E --> F[Write compact scanline spans]
|
||||
F --> G[Invoke row handler]
|
||||
```
|
||||
|
||||
The rasterizer therefore emits only rows that actually received contributions and only the non-zero spans within those rows.
|
||||
|
||||
## Fill Rules
|
||||
|
||||
The rasterizer supports both `NonZero` and `EvenOdd`.
|
||||
|
||||
### NonZero
|
||||
|
||||
The accumulated signed area is treated as winding magnitude. Coverage is the clamped absolute value of that area.
|
||||
|
||||
### EvenOdd
|
||||
|
||||
The accumulated area is wrapped into the even-odd domain before coverage is produced. This gives parity-based behavior without changing the earlier scan-conversion logic.
|
||||
|
||||
The fill rule is therefore an emission-time decision, not a geometry-preprocessing decision.
|
||||
|
||||
## Antialiased And Aliased Modes
|
||||
|
||||
The rasterizer can emit either continuous or thresholded coverage.
|
||||
|
||||
- `Antialiased` mode keeps the continuous coverage produced by the area-and-cover math
|
||||
- `Aliased` mode thresholds that continuous coverage using `AntialiasThreshold`
|
||||
|
||||
The scan-conversion core stays the same in both modes. Only the final conversion from area to emitted coverage changes.
|
||||
|
||||
## Why Self-Intersections Work
|
||||
|
||||
The rasterizer can handle self-intersections because it does not require geometric boolean normalization before rasterization. It accumulates signed contributions and then applies the selected fill rule during emission.
|
||||
|
||||
That means overlapping or self-crossing contours are resolved by:
|
||||
|
||||
- area-and-cover integration
|
||||
- winding or parity mapping
|
||||
|
||||
instead of by an earlier polygon-boolean pass.
|
||||
|
||||
## How The Rasterizer Stays Separate From The Backend
|
||||
|
||||
The rasterizer and the backend solve different problems.
|
||||
|
||||
The rasterizer decides:
|
||||
|
||||
- how geometry contributes coverage
|
||||
- which rows and columns within a band are touched
|
||||
- how much coverage each emitted span has
|
||||
|
||||
The backend decides:
|
||||
|
||||
- which scene items execute
|
||||
- which retained band is being scanned
|
||||
- which destination slice receives the coverage
|
||||
- which brush renderer consumes the emitted spans
|
||||
|
||||
That separation is one of the main architectural advantages of the current CPU path.
|
||||
|
||||
## Reading Guide
|
||||
|
||||
If you are new to this part of the library, read the rasterizer in this order:
|
||||
|
||||
1. `DrawingCanvas.cs`
|
||||
2. `DrawingCanvas{TPixel}.cs`
|
||||
3. `DrawingCanvasBatcher{TPixel}.cs`
|
||||
4. `DefaultDrawingBackend.cs`
|
||||
5. `FlushScene.cs`
|
||||
6. `CreateRasterizableGeometry(...)` in `DefaultRasterizer.cs`
|
||||
7. `Linearizer<TL>` and the concrete linearizers in `DefaultRasterizer.Linearizer.cs`
|
||||
8. retained line types in `DefaultRasterizer.RetainedTypes.cs`
|
||||
9. `ExecuteRasterizableBand(...)` in `DefaultRasterizer.cs`
|
||||
10. `Context` in `DefaultRasterizer.cs`
|
||||
|
||||
That order mirrors the data lifecycle:
|
||||
|
||||
canvas intent -> prepared geometry -> retained storage -> band execution -> coverage emission
|
||||
|
||||
## The Mental Model To Keep
|
||||
|
||||
The easiest way to reason about `DefaultRasterizer` is this:
|
||||
|
||||
it is a retained fixed-point polygon scanner that transforms prepared geometry into compact band-local line payloads, then turns those payloads into row coverage spans.
|
||||
|
||||
If that model stays clear, the rest of the code becomes easier to read:
|
||||
|
||||
- the canvas and backend docs explain how execution reaches the CPU path
|
||||
- the linearizer explains where retained line data comes from
|
||||
- `RasterizableGeometry` explains what is stored
|
||||
- the `Context` explains how retained data becomes coverage
|
||||
- the backend explains how coverage becomes pixels
|
||||
@ -0,0 +1,202 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region.
|
||||
/// </summary>
|
||||
public sealed partial class DefaultDrawingBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// Adapts rasterizer coverage callbacks into brush application against the active band target.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private readonly struct FillCoverageRowHandler<TPixel> : IRasterizerCoverageRowHandler
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly BrushRenderer<TPixel> renderer;
|
||||
private readonly BandTarget<TPixel> target;
|
||||
private readonly BrushWorkspace<TPixel> brushWorkspace;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FillCoverageRowHandler{TPixel}"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="renderer">The brush renderer that will consume emitted coverage spans.</param>
|
||||
/// <param name="target">The active band target being rendered.</param>
|
||||
/// <param name="brushWorkspace">The worker-local brush workspace.</param>
|
||||
public FillCoverageRowHandler(
|
||||
BrushRenderer<TPixel> renderer,
|
||||
BandTarget<TPixel> target,
|
||||
BrushWorkspace<TPixel> brushWorkspace)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.target = target;
|
||||
this.brushWorkspace = brushWorkspace;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies one emitted coverage span to the active destination band.
|
||||
/// </summary>
|
||||
/// <param name="y">The absolute destination row.</param>
|
||||
/// <param name="startX">The absolute start column of the coverage span.</param>
|
||||
/// <param name="coverage">The emitted coverage values.</param>
|
||||
public void Handle(int y, int startX, Span<float> coverage)
|
||||
{
|
||||
int localY = y - this.target.AbsoluteTop;
|
||||
if ((uint)localY >= (uint)this.target.Region.Height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int clipStartX = Math.Max(startX, this.target.AbsoluteLeft);
|
||||
int clipEndX = Math.Min(startX + coverage.Length, this.target.AbsoluteLeft + this.target.Region.Width);
|
||||
if (clipEndX <= clipStartX)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The rasterizer emits absolute coordinates; clip them once here so the brush
|
||||
// renderer can operate against a tight destination span with no extra bounds work.
|
||||
int coverageOffset = clipStartX - startX;
|
||||
int clippedLength = clipEndX - clipStartX;
|
||||
Span<TPixel> destinationRow = this.target.Region
|
||||
.DangerousGetRowSpan(localY)
|
||||
.Slice(clipStartX - this.target.AbsoluteLeft, clippedLength);
|
||||
this.renderer.Apply(destinationRow, coverage.Slice(coverageOffset, clippedLength), clipStartX, y, this.brushWorkspace);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one active composition target for a retained row.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class BandTarget<TPixel> : IDisposable
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly Buffer2D<TPixel>? owner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BandTarget{TPixel}"/> class over an existing region.
|
||||
/// </summary>
|
||||
/// <param name="region">The destination region.</param>
|
||||
/// <param name="absoluteLeft">The absolute X origin of the region.</param>
|
||||
/// <param name="absoluteTop">The absolute Y origin of the region.</param>
|
||||
/// <param name="graphicsOptions">The graphics options used when this target is later composited.</param>
|
||||
public BandTarget(Buffer2DRegion<TPixel> region, int absoluteLeft, int absoluteTop, GraphicsOptions? graphicsOptions)
|
||||
{
|
||||
this.Region = region;
|
||||
this.AbsoluteLeft = absoluteLeft;
|
||||
this.AbsoluteTop = absoluteTop;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BandTarget{TPixel}"/> class over an owned temporary buffer.
|
||||
/// </summary>
|
||||
/// <param name="owner">The owned buffer backing the target.</param>
|
||||
/// <param name="bounds">The absolute bounds represented by the target.</param>
|
||||
/// <param name="graphicsOptions">The graphics options used when this target is later composited.</param>
|
||||
public BandTarget(Buffer2D<TPixel> owner, Rectangle bounds, GraphicsOptions? graphicsOptions)
|
||||
{
|
||||
this.owner = owner;
|
||||
this.Region = owner.GetRegion();
|
||||
this.AbsoluteLeft = bounds.X;
|
||||
this.AbsoluteTop = bounds.Y;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the writable pixel region for the target.
|
||||
/// </summary>
|
||||
public Buffer2DRegion<TPixel> Region { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute X origin of <see cref="Region"/>.
|
||||
/// </summary>
|
||||
public int AbsoluteLeft { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute Y origin of <see cref="Region"/>.
|
||||
/// </summary>
|
||||
public int AbsoluteTop { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options associated with the target when it is used as a layer.
|
||||
/// </summary>
|
||||
public GraphicsOptions? GraphicsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Releases the owned temporary buffer when the target represents a layer.
|
||||
/// </summary>
|
||||
public void Dispose() => this.owner?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the reusable worker-local scratch used while executing retained scene rows.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class WorkerState<TPixel> : IDisposable
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly MemoryAllocator allocator;
|
||||
private DefaultRasterizer.WorkerScratch? scratch;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkerState{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allocator">The memory allocator used for scratch growth.</param>
|
||||
/// <param name="destinationWidth">The destination width used to size the brush workspace.</param>
|
||||
/// <param name="layerDepth">The maximum retained layer depth required by the scene.</param>
|
||||
public WorkerState(
|
||||
MemoryAllocator allocator,
|
||||
int destinationWidth,
|
||||
int layerDepth)
|
||||
{
|
||||
this.allocator = allocator;
|
||||
this.BrushWorkspace = new BrushWorkspace<TPixel>(allocator, destinationWidth);
|
||||
this.TargetStack = new BandTarget<TPixel>[layerDepth];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reusable brush workspace for the worker.
|
||||
/// </summary>
|
||||
public BrushWorkspace<TPixel> BrushWorkspace { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reusable composition target stack for the worker.
|
||||
/// </summary>
|
||||
public BandTarget<TPixel>[] TargetStack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a reusable raster scratch instance sized for the requested width.
|
||||
/// </summary>
|
||||
/// <param name="requiredWidth">The minimum scanline width required by the current row.</param>
|
||||
/// <returns>A scratch instance that can execute the row.</returns>
|
||||
public DefaultRasterizer.WorkerScratch GetOrCreateScratch(int requiredWidth)
|
||||
{
|
||||
DefaultRasterizer.WorkerScratch? current = this.scratch;
|
||||
if (current is not null && current.CanReuse(requiredWidth))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
current?.Dispose();
|
||||
this.scratch = DefaultRasterizer.CreateWorkerScratch(this.allocator, requiredWidth);
|
||||
return this.scratch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the worker-local scratch and brush workspace.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.scratch?.Dispose();
|
||||
this.BrushWorkspace.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
483
ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs
Normal file
483
ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs
Normal file
@ -0,0 +1,483 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region.
|
||||
/// </summary>
|
||||
public sealed partial class DefaultDrawingBackend : IDrawingBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default backend instance.
|
||||
/// </summary>
|
||||
public static DefaultDrawingBackend Instance { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public DrawingBackendScene CreateScene(
|
||||
Configuration configuration,
|
||||
Rectangle targetBounds,
|
||||
DrawingCommandBatch commandBatch,
|
||||
IReadOnlyList<IDisposable>? ownedResources = null)
|
||||
{
|
||||
FlushScene scene = FlushScene.Create(
|
||||
commandBatch,
|
||||
targetBounds,
|
||||
configuration.MemoryAllocator,
|
||||
configuration.MaxDegreeOfParallelism);
|
||||
|
||||
return new DefaultDrawingBackendScene(scene, targetBounds, ownedResources);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RenderScene<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
DrawingBackendScene scene)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
if (scene is not DefaultDrawingBackendScene cpuScene)
|
||||
{
|
||||
throw new InvalidOperationException("The retained scene is not a CPU drawing backend scene.");
|
||||
}
|
||||
|
||||
if (!target.TryGetCpuRegion(out Buffer2DRegion<TPixel> destinationFrame))
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets.");
|
||||
}
|
||||
|
||||
if (target.Bounds != cpuScene.Bounds)
|
||||
{
|
||||
throw new InvalidOperationException("The target bounds do not match the retained CPU scene bounds.");
|
||||
}
|
||||
|
||||
if (cpuScene.Scene is FlushScene flushScene && flushScene.RowCount != 0)
|
||||
{
|
||||
ExecuteScene(configuration, destinationFrame, flushScene);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one retained flush scene against a CPU destination frame.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="destinationFrame">The destination CPU region.</param>
|
||||
/// <param name="scene">The retained scene to execute.</param>
|
||||
private static void ExecuteScene<TPixel>(
|
||||
Configuration configuration,
|
||||
Buffer2DRegion<TPixel> destinationFrame,
|
||||
FlushScene scene)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
// Warm the cached renderers before the row loop so the hot execution path only
|
||||
// performs retained-scene work and brush application.
|
||||
if (scene.FillItemCount > 0)
|
||||
{
|
||||
for (int i = 0; i < scene.FillItems.Length; i++)
|
||||
{
|
||||
if (scene.FillItems[i] is FlushScene.FillSceneItem item)
|
||||
{
|
||||
_ = item.GetRenderer<TPixel>(configuration, destinationFrame.Width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scene.StrokeItemCount > 0)
|
||||
{
|
||||
for (int i = 0; i < scene.StrokeItems.Length; i++)
|
||||
{
|
||||
if (scene.StrokeItems[i] is FlushScene.StrokeSceneItem item)
|
||||
{
|
||||
_ = item.GetRenderer<TPixel>(configuration, destinationFrame.Width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int requestedParallelism = configuration.MaxDegreeOfParallelism;
|
||||
_ = Parallel.For(
|
||||
fromInclusive: 0,
|
||||
toExclusive: scene.RowCount,
|
||||
parallelOptions: ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, scene.RowCount),
|
||||
localInit: () => new WorkerState<TPixel>(configuration.MemoryAllocator, destinationFrame.Width, scene.MaxLayerDepth + 1),
|
||||
body: (rowIndex, _, state) =>
|
||||
{
|
||||
ExecuteSceneRow(
|
||||
configuration,
|
||||
destinationFrame,
|
||||
scene,
|
||||
scene.Rows[rowIndex],
|
||||
state);
|
||||
|
||||
return state;
|
||||
},
|
||||
localFinally: static state => state.Dispose());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one retained scene row against the destination band it overlaps.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="destinationFrame">The destination CPU region.</param>
|
||||
/// <param name="scene">The retained flush scene.</param>
|
||||
/// <param name="row">The retained scene row to execute.</param>
|
||||
/// <param name="state">The worker-local scratch and compositing state.</param>
|
||||
private static void ExecuteSceneRow<TPixel>(
|
||||
Configuration configuration,
|
||||
Buffer2DRegion<TPixel> destinationFrame,
|
||||
FlushScene scene,
|
||||
in FlushScene.SceneRow row,
|
||||
WorkerState<TPixel> state)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
int bandTop = row.RowBandIndex * DefaultRasterizer.DefaultTileHeight;
|
||||
int localBandTop = bandTop - destinationFrame.Bounds.Y;
|
||||
int bandHeight = Math.Min(DefaultRasterizer.DefaultTileHeight, destinationFrame.Height - localBandTop);
|
||||
if (bandHeight <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Buffer2DRegion<TPixel> destinationBand = destinationFrame.GetSubRegion(0, localBandTop, destinationFrame.Width, bandHeight);
|
||||
BandTarget<TPixel>[] targetStack = state.TargetStack;
|
||||
int targetCount = 1;
|
||||
targetStack[0] = new BandTarget<TPixel>(destinationBand, destinationFrame.Bounds.X, bandTop, null);
|
||||
int scratchWidth = GetRowScratchWidth(scene, row, destinationFrame.Width);
|
||||
DefaultRasterizer.WorkerScratch scratch = state.GetOrCreateScratch(scratchWidth);
|
||||
|
||||
try
|
||||
{
|
||||
for (FlushScene.SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next)
|
||||
{
|
||||
foreach (FlushScene.SceneOperation operation in block.Items)
|
||||
{
|
||||
// Each retained row contains a compact mix of layer control operations and
|
||||
// draw operations in original command order, so the executor can replay the
|
||||
// row without re-walking the full scene description.
|
||||
switch (operation.Kind)
|
||||
{
|
||||
case FlushScene.SceneOperationKind.BeginLayer:
|
||||
GraphicsOptions? layerOptions = scene.LayerOptions[operation.ItemIndex];
|
||||
|
||||
targetStack[targetCount++] =
|
||||
new BandTarget<TPixel>(
|
||||
configuration.MemoryAllocator.Allocate2D<TPixel>(operation.LayerBounds.Width, operation.LayerBounds.Height, AllocationOptions.Clean),
|
||||
operation.LayerBounds,
|
||||
layerOptions);
|
||||
break;
|
||||
|
||||
case FlushScene.SceneOperationKind.EndLayer:
|
||||
BandTarget<TPixel> source = targetStack[--targetCount];
|
||||
BandTarget<TPixel> destination = targetStack[targetCount - 1];
|
||||
CompositeLayerBand(configuration, source, destination, state.BrushWorkspace);
|
||||
source.Dispose();
|
||||
break;
|
||||
|
||||
case FlushScene.SceneOperationKind.FillItem:
|
||||
BandTarget<TPixel> target = targetStack[targetCount - 1];
|
||||
FlushScene.FillSceneItem sceneItem = scene.FillItems[operation.ItemIndex]!;
|
||||
ExecuteFillOperation(
|
||||
sceneItem.GetRenderer<TPixel>(configuration, destinationFrame.Width),
|
||||
new DefaultRasterizer.RasterizableItem(sceneItem.Rasterizable, operation.LocalRowIndex),
|
||||
target,
|
||||
scratch,
|
||||
state);
|
||||
break;
|
||||
|
||||
case FlushScene.SceneOperationKind.StrokeItem:
|
||||
BandTarget<TPixel> strokeTarget = targetStack[targetCount - 1];
|
||||
FlushScene.StrokeSceneItem strokeSceneItem = scene.StrokeItems[operation.ItemIndex]!;
|
||||
ExecuteStrokeOperation(
|
||||
strokeSceneItem.GetRenderer<TPixel>(configuration, destinationFrame.Width),
|
||||
new DefaultRasterizer.StrokeRasterizableItem(strokeSceneItem.Rasterizable, operation.LocalRowIndex),
|
||||
strokeTarget,
|
||||
scratch,
|
||||
state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (int i = 1; i < targetCount; i++)
|
||||
{
|
||||
targetStack[i].Dispose();
|
||||
targetStack[i] = null!;
|
||||
}
|
||||
|
||||
targetStack[0] = null!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum reusable scratch width needed to execute one retained scene row.
|
||||
/// </summary>
|
||||
/// <param name="scene">The retained flush scene.</param>
|
||||
/// <param name="row">The retained scene row.</param>
|
||||
/// <param name="minimumWidth">The baseline width taken from the destination band.</param>
|
||||
/// <returns>The scratch width required by the row.</returns>
|
||||
private static int GetRowScratchWidth(
|
||||
FlushScene scene,
|
||||
in FlushScene.SceneRow row,
|
||||
int minimumWidth)
|
||||
{
|
||||
int width = minimumWidth;
|
||||
for (FlushScene.SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next)
|
||||
{
|
||||
foreach (FlushScene.SceneOperation operation in block.Items)
|
||||
{
|
||||
if (operation.Kind is FlushScene.SceneOperationKind.BeginLayer or FlushScene.SceneOperationKind.EndLayer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int itemWidth = operation.Kind == FlushScene.SceneOperationKind.FillItem
|
||||
? scene.FillItems[operation.ItemIndex]!.Rasterizable.Width
|
||||
: scene.StrokeItems[operation.ItemIndex]!.Rasterizable.Width;
|
||||
if (itemWidth > width)
|
||||
{
|
||||
width = itemWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one retained fill operation through the rasterizer and brush renderer.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="renderer">The memoized brush renderer for the scene item.</param>
|
||||
/// <param name="item">The retained rasterizable row item to execute.</param>
|
||||
/// <param name="target">The active composition target for the row.</param>
|
||||
/// <param name="scratch">The worker-local raster scratch.</param>
|
||||
/// <param name="state">The worker-local execution state.</param>
|
||||
private static void ExecuteFillOperation<TPixel>(
|
||||
BrushRenderer<TPixel> renderer,
|
||||
DefaultRasterizer.RasterizableItem item,
|
||||
BandTarget<TPixel> target,
|
||||
DefaultRasterizer.WorkerScratch scratch,
|
||||
WorkerState<TPixel> state)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex);
|
||||
DefaultRasterizer.Context context = scratch.CreateContext(
|
||||
bandInfo.IntersectionRule,
|
||||
bandInfo.RasterizationMode,
|
||||
bandInfo.AntialiasThreshold);
|
||||
FillCoverageRowHandler<TPixel> rowHandler = new(renderer, target, state.BrushWorkspace);
|
||||
DefaultRasterizer.ExecuteRasterizableItem(
|
||||
ref context,
|
||||
in item,
|
||||
in bandInfo,
|
||||
scratch.Scanline,
|
||||
ref rowHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one retained stroke operation through the rasterizer and brush renderer.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="renderer">The memoized brush renderer for the scene item.</param>
|
||||
/// <param name="item">The retained stroke rasterizable row item to execute.</param>
|
||||
/// <param name="target">The active composition target for the row.</param>
|
||||
/// <param name="scratch">The worker-local raster scratch.</param>
|
||||
/// <param name="state">The worker-local execution state.</param>
|
||||
private static void ExecuteStrokeOperation<TPixel>(
|
||||
BrushRenderer<TPixel> renderer,
|
||||
DefaultRasterizer.StrokeRasterizableItem item,
|
||||
BandTarget<TPixel> target,
|
||||
DefaultRasterizer.WorkerScratch scratch,
|
||||
WorkerState<TPixel> state)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex);
|
||||
DefaultRasterizer.Context context = scratch.CreateContext(
|
||||
bandInfo.IntersectionRule,
|
||||
bandInfo.RasterizationMode,
|
||||
bandInfo.AntialiasThreshold);
|
||||
FillCoverageRowHandler<TPixel> rowHandler = new(renderer, target, state.BrushWorkspace);
|
||||
Span<float> strokeBandCoverage = item.Rasterizable.RequiresBandCoverage ? scratch.StrokeBandCoverage : [];
|
||||
DefaultRasterizer.ExecuteStrokeRasterizableItem(
|
||||
ref context,
|
||||
in item,
|
||||
in bandInfo,
|
||||
scratch.Scanline,
|
||||
strokeBandCoverage,
|
||||
ref rowHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composites one temporary layer band back into its destination band.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="source">The source layer band.</param>
|
||||
/// <param name="destination">The destination band to blend into.</param>
|
||||
/// <param name="brushWorkspace">The worker-local amount buffer workspace.</param>
|
||||
private static void CompositeLayerBand<TPixel>(
|
||||
Configuration configuration,
|
||||
BandTarget<TPixel> source,
|
||||
BandTarget<TPixel> destination,
|
||||
BrushWorkspace<TPixel> brushWorkspace)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
int width = source.Region.Width;
|
||||
if (width == 0 || source.Region.Height == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Rectangle overlap = Rectangle.Intersect(
|
||||
new Rectangle(source.AbsoluteLeft, source.AbsoluteTop, source.Region.Width, source.Region.Height),
|
||||
new Rectangle(destination.AbsoluteLeft, destination.AbsoluteTop, destination.Region.Width, destination.Region.Height));
|
||||
|
||||
if (overlap.Width <= 0 || overlap.Height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.GraphicsOptions is not GraphicsOptions graphicsOptions)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(graphicsOptions);
|
||||
Span<float> amounts = brushWorkspace.GetAmounts(overlap.Width);
|
||||
amounts[..overlap.Width].Fill(graphicsOptions.BlendPercentage);
|
||||
|
||||
int sourceOffsetX = overlap.X - source.AbsoluteLeft;
|
||||
int sourceOffsetY = overlap.Y - source.AbsoluteTop;
|
||||
int destinationOffsetX = overlap.X - destination.AbsoluteLeft;
|
||||
int destinationOffsetY = overlap.Y - destination.AbsoluteTop;
|
||||
|
||||
// Blend the overlapping rows only; the retained scene has already clipped the layer
|
||||
// bounds so there is no need for extra per-pixel bounds logic here.
|
||||
for (int y = 0; y < overlap.Height; y++)
|
||||
{
|
||||
Span<TPixel> sourceRow = source.Region.DangerousGetRowSpan(sourceOffsetY + y).Slice(sourceOffsetX, overlap.Width);
|
||||
Span<TPixel> destinationRow = destination.Region.DangerousGetRowSpan(destinationOffsetY + y).Slice(destinationOffsetX, overlap.Width);
|
||||
blender.Blend(
|
||||
configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
sourceRow,
|
||||
amounts[..overlap.Width],
|
||||
brushWorkspace.GetBlendScratch(overlap.Width, 3));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composites one CPU-backed frame onto another using the supplied graphics options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="source">The source frame.</param>
|
||||
/// <param name="destination">The destination frame.</param>
|
||||
/// <param name="destinationOffset">The destination offset relative to <paramref name="destination"/>.</param>
|
||||
/// <param name="options">The graphics options controlling composition.</param>
|
||||
public static void ComposeLayer<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> source,
|
||||
ICanvasFrame<TPixel> destination,
|
||||
Point destinationOffset,
|
||||
GraphicsOptions options)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
Guard.NotNull(configuration, nameof(configuration));
|
||||
|
||||
if (!source.TryGetCpuRegion(out Buffer2DRegion<TPixel> sourceRegion))
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible source frames.");
|
||||
}
|
||||
|
||||
if (!destination.TryGetCpuRegion(out Buffer2DRegion<TPixel> destinationRegion))
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible destination frames.");
|
||||
}
|
||||
|
||||
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(options);
|
||||
float blendPercentage = options.BlendPercentage;
|
||||
|
||||
int srcWidth = sourceRegion.Width;
|
||||
int srcHeight = sourceRegion.Height;
|
||||
int dstWidth = destinationRegion.Width;
|
||||
int dstHeight = destinationRegion.Height;
|
||||
|
||||
// Clamp the compositing region to both source and destination bounds.
|
||||
int startX = Math.Max(0, -destinationOffset.X);
|
||||
int startY = Math.Max(0, -destinationOffset.Y);
|
||||
int endX = Math.Min(srcWidth, dstWidth - destinationOffset.X);
|
||||
int endY = Math.Min(srcHeight, dstHeight - destinationOffset.Y);
|
||||
|
||||
if (endX <= startX || endY <= startY)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int width = endX - startX;
|
||||
|
||||
// Allocate a reusable per-row amount buffer from the memory pool.
|
||||
using IMemoryOwner<float> amountsOwner = configuration.MemoryAllocator.Allocate<float>(width);
|
||||
Span<float> amounts = amountsOwner.Memory.Span;
|
||||
amounts.Fill(blendPercentage);
|
||||
|
||||
for (int y = startY; y < endY; y++)
|
||||
{
|
||||
Span<TPixel> srcRow = sourceRegion.DangerousGetRowSpan(y).Slice(startX, width);
|
||||
int dstX = destinationOffset.X + startX;
|
||||
int dstY = destinationOffset.Y + y;
|
||||
Span<TPixel> dstRow = destinationRegion.DangerousGetRowSpan(dstY).Slice(dstX, width);
|
||||
|
||||
blender.Blend(configuration, dstRow, dstRow, srcRow, amounts);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReadRegion<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
Rectangle sourceRectangle,
|
||||
Buffer2DRegion<TPixel> destination)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
Guard.NotNull(configuration, nameof(configuration));
|
||||
Guard.NotNull(destination.Buffer, nameof(destination));
|
||||
|
||||
// CPU backend readback is available only when the target exposes CPU pixels.
|
||||
if (!target.TryGetCpuRegion(out Buffer2DRegion<TPixel> sourceRegion))
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for readback.");
|
||||
}
|
||||
|
||||
// Clamp the request to the target region to avoid out-of-range row slicing.
|
||||
Rectangle clipped = Rectangle.Intersect(
|
||||
new Rectangle(0, 0, sourceRegion.Width, sourceRegion.Height),
|
||||
sourceRectangle);
|
||||
|
||||
if (clipped.Width <= 0 || clipped.Height <= 0)
|
||||
{
|
||||
throw new ArgumentException("The requested readback rectangle does not intersect the target bounds.", nameof(sourceRectangle));
|
||||
}
|
||||
|
||||
int copyWidth = Math.Min(clipped.Width, destination.Width);
|
||||
int copyHeight = Math.Min(clipped.Height, destination.Height);
|
||||
|
||||
for (int y = 0; y < copyHeight; y++)
|
||||
{
|
||||
sourceRegion.DangerousGetRowSpan(clipped.Y + y)
|
||||
.Slice(clipped.X, copyWidth)
|
||||
.CopyTo(destination.DangerousGetRowSpan(y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Retained scene created by the CPU drawing backend.
|
||||
/// </summary>
|
||||
public sealed class DefaultDrawingBackendScene : DrawingBackendScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultDrawingBackendScene"/> class.
|
||||
/// </summary>
|
||||
/// <param name="scene">The retained CPU flush scene.</param>
|
||||
/// <param name="bounds">The target bounds used to create the scene.</param>
|
||||
/// <param name="ownedResources">Resources that must stay alive for the retained scene.</param>
|
||||
internal DefaultDrawingBackendScene(
|
||||
FlushScene scene,
|
||||
Rectangle bounds,
|
||||
IReadOnlyList<IDisposable>? ownedResources)
|
||||
: base(bounds, ownedResources)
|
||||
=> this.Scene = scene;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained CPU flush scene when this is a leaf scene.
|
||||
/// </summary>
|
||||
internal FlushScene? Scene { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DisposeCore()
|
||||
=> this.Scene?.Dispose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Buffers;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Contract implemented by retained line-block payloads.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSelf">The concrete retained line-block type.</typeparam>
|
||||
internal interface ILineBlock<TSelf>
|
||||
where TSelf : class, ILineBlock<TSelf>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of lines stored in a full block.
|
||||
/// </summary>
|
||||
public static abstract int LineCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next block in the retained chain.
|
||||
/// </summary>
|
||||
public TSelf? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Rasterizes the leading <paramref name="count"/> lines from this block.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of leading lines to rasterize from this block.</param>
|
||||
/// <param name="context">The mutable scan-conversion context to write into.</param>
|
||||
public void Rasterize(int count, ref Context context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retained tile-space bounds for one linearized geometry payload.
|
||||
/// </summary>
|
||||
internal readonly struct TileBounds
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TileBounds"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="x">The tile-space left coordinate.</param>
|
||||
/// <param name="y">The tile-space top coordinate.</param>
|
||||
/// <param name="columnCount">The tile-space column count.</param>
|
||||
/// <param name="rowCount">The tile-space row count.</param>
|
||||
public TileBounds(int x, int y, int columnCount, int rowCount)
|
||||
{
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
this.ColumnCount = columnCount;
|
||||
this.RowCount = rowCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space left coordinate.
|
||||
/// </summary>
|
||||
public int X { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space top coordinate.
|
||||
/// </summary>
|
||||
public int Y { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space column count.
|
||||
/// </summary>
|
||||
public int ColumnCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space row count.
|
||||
/// </summary>
|
||||
public int RowCount { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the finalized retained raster payload for one line-block encoding.
|
||||
/// </summary>
|
||||
/// <typeparam name="TLineBlock">The concrete retained line-block type.</typeparam>
|
||||
internal sealed class LinearizedRasterData<TLineBlock>
|
||||
where TLineBlock : class, ILineBlock<TLineBlock>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearizedRasterData{TLineBlock}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="geometry">The source linear geometry.</param>
|
||||
/// <param name="bounds">The retained tile-space bounds.</param>
|
||||
/// <param name="lines">The retained line-block chain for each row band.</param>
|
||||
/// <param name="firstBlockLineCounts">The valid line count in each row's front block.</param>
|
||||
/// <param name="startCoverTable">The retained start-cover seeds for each row band.</param>
|
||||
public LinearizedRasterData(
|
||||
LinearGeometry geometry,
|
||||
TileBounds bounds,
|
||||
TLineBlock?[] lines,
|
||||
int[] firstBlockLineCounts,
|
||||
IMemoryOwner<int>?[] startCoverTable)
|
||||
{
|
||||
this.Geometry = geometry;
|
||||
this.Bounds = bounds;
|
||||
this.Lines = lines;
|
||||
this.FirstBlockLineCounts = firstBlockLineCounts;
|
||||
this.StartCoverTable = startCoverTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source linear geometry.
|
||||
/// </summary>
|
||||
public LinearGeometry Geometry { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained tile-space bounds.
|
||||
/// </summary>
|
||||
public TileBounds Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained line-block chain for each row band.
|
||||
/// </summary>
|
||||
public TLineBlock?[] Lines { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the valid front-block line count for each row band.
|
||||
/// </summary>
|
||||
public int[] FirstBlockLineCounts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover seeds for each row band.
|
||||
/// </summary>
|
||||
public IMemoryOwner<int>?[] StartCoverTable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the retained line blocks for one row band.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">The row band index to iterate.</param>
|
||||
/// <param name="context">The mutable scan-conversion context.</param>
|
||||
public void Iterate(int rowIndex, ref Context context)
|
||||
{
|
||||
int count = this.FirstBlockLineCounts[rowIndex];
|
||||
TLineBlock? lineBlock = this.Lines[rowIndex];
|
||||
while (lineBlock is not null)
|
||||
{
|
||||
lineBlock.Rasterize(count, ref context);
|
||||
lineBlock = lineBlock.Next;
|
||||
count = TLineBlock.LineCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,920 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class that lowers translated geometry into retained per-row line storage.
|
||||
/// </summary>
|
||||
/// <typeparam name="TL">The mutable per-row line collector type.</typeparam>
|
||||
private abstract class Linearizer<TL>
|
||||
where TL : class
|
||||
{
|
||||
private bool hasAnyCoverage;
|
||||
|
||||
protected Linearizer(
|
||||
LinearGeometry geometry,
|
||||
Matrix4x4 residual,
|
||||
int translateX,
|
||||
int translateY,
|
||||
int minX,
|
||||
int minY,
|
||||
int width,
|
||||
int height,
|
||||
int firstBandIndex,
|
||||
int rowBandCount,
|
||||
float samplingOffsetX,
|
||||
float samplingOffsetY,
|
||||
MemoryAllocator allocator)
|
||||
{
|
||||
this.Geometry = geometry;
|
||||
this.Residual = residual;
|
||||
this.HasResidual = !residual.IsIdentity;
|
||||
this.TranslateX = translateX;
|
||||
this.TranslateY = translateY;
|
||||
this.MinX = minX;
|
||||
this.MinY = minY;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
this.FirstBandIndex = firstBandIndex;
|
||||
this.RowBandCount = rowBandCount;
|
||||
this.SamplingOffsetX = samplingOffsetX;
|
||||
this.SamplingOffsetY = samplingOffsetY;
|
||||
this.Allocator = allocator;
|
||||
this.BandTopStart = (firstBandIndex * PreferredRowHeight) - minY;
|
||||
this.FirstBlockLineCounts = new int[rowBandCount];
|
||||
this.LineCounts = new int[rowBandCount];
|
||||
this.StartCoverTable = new IMemoryOwner<int>?[rowBandCount];
|
||||
this.LineArrays = new TL?[rowBandCount];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source geometry being lowered.
|
||||
/// </summary>
|
||||
protected LinearGeometry Geometry { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the residual transform applied to each source point during emission.
|
||||
/// </summary>
|
||||
protected Matrix4x4 Residual { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether <see cref="Residual"/> is non-identity.
|
||||
/// </summary>
|
||||
protected bool HasResidual { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the translated X offset applied to the geometry.
|
||||
/// </summary>
|
||||
protected int TranslateX { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the translated Y offset applied to the geometry.
|
||||
/// </summary>
|
||||
protected int TranslateY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum destination X bound after clipping.
|
||||
/// </summary>
|
||||
protected int MinX { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum destination Y bound after clipping.
|
||||
/// </summary>
|
||||
protected int MinY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible destination width in pixels.
|
||||
/// </summary>
|
||||
protected int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible destination height in pixels.
|
||||
/// </summary>
|
||||
protected int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first retained row-band index touched by the geometry.
|
||||
/// </summary>
|
||||
protected int FirstBandIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of retained row bands owned by the geometry.
|
||||
/// </summary>
|
||||
protected int RowBandCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the horizontal sampling offset applied before fixed-point conversion.
|
||||
/// </summary>
|
||||
protected float SamplingOffsetX { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the vertical sampling offset applied before fixed-point conversion.
|
||||
/// </summary>
|
||||
protected float SamplingOffsetY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the allocator used for retained start-cover storage.
|
||||
/// </summary>
|
||||
protected MemoryAllocator Allocator { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top offset, in whole pixels, of the first retained row band.
|
||||
/// </summary>
|
||||
protected int BandTopStart { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mutable per-row line collectors used during lowering.
|
||||
/// </summary>
|
||||
protected TL?[] LineArrays { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the valid front-block line count for each retained row band.
|
||||
/// </summary>
|
||||
protected int[] FirstBlockLineCounts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total retained line count for each row band.
|
||||
/// </summary>
|
||||
protected int[] LineCounts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover storage for each row band.
|
||||
/// </summary>
|
||||
protected IMemoryOwner<int>?[] StartCoverTable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether any retained payload was produced.
|
||||
/// </summary>
|
||||
protected ref bool HasAnyCoverage => ref this.hasAnyCoverage;
|
||||
|
||||
/// <summary>
|
||||
/// Executes the linearization pass and finalizes the retained row payloads.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> when any retained coverage was produced; otherwise <see langword="false"/>.</returns>
|
||||
protected virtual bool ProcessCore()
|
||||
{
|
||||
RectangleF translatedBounds = this.HasResidual
|
||||
? RectangleF.Transform(this.Geometry.Info.Bounds, this.Residual)
|
||||
: this.Geometry.Info.Bounds;
|
||||
translatedBounds.Offset(this.TranslateX + this.SamplingOffsetX - this.MinX, this.TranslateY + this.SamplingOffsetY - this.MinY);
|
||||
|
||||
bool contains =
|
||||
translatedBounds.Left >= 0F &&
|
||||
translatedBounds.Top >= 0F &&
|
||||
translatedBounds.Right <= this.Width &&
|
||||
translatedBounds.Bottom <= this.Height;
|
||||
|
||||
// Contained geometry can skip clipping and go straight to the fixed-point band splitter.
|
||||
if (contains)
|
||||
{
|
||||
this.ProcessContained();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Geometry that touches the interest edges needs clipping so start covers and line
|
||||
// segments still match the destination bounds seen by the rasterizer.
|
||||
this.ProcessUncontained();
|
||||
}
|
||||
|
||||
if (!this.hasAnyCoverage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.FinalizeLines();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizes geometry that is fully contained inside the destination interest.
|
||||
/// </summary>
|
||||
protected void ProcessContained()
|
||||
{
|
||||
SegmentEnumerator enumerator = this.Geometry.GetSegments();
|
||||
Matrix4x4 residual = this.Residual;
|
||||
bool hasResidual = this.HasResidual;
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
LinearSegment segment = enumerator.Current;
|
||||
PointF p0 = segment.Start;
|
||||
PointF p1 = segment.End;
|
||||
if (hasResidual)
|
||||
{
|
||||
p0 = PointF.Transform(p0, residual);
|
||||
p1 = PointF.Transform(p1, residual);
|
||||
}
|
||||
|
||||
this.AddContainedLineF24Dot8(
|
||||
FloatToFixed24Dot8(((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX),
|
||||
FloatToFixed24Dot8(((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY),
|
||||
FloatToFixed24Dot8(((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX),
|
||||
FloatToFixed24Dot8(((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizes geometry that intersects the destination interest bounds and requires clipping.
|
||||
/// </summary>
|
||||
protected void ProcessUncontained()
|
||||
{
|
||||
SegmentEnumerator enumerator = this.Geometry.GetSegments();
|
||||
Matrix4x4 residual = this.Residual;
|
||||
bool hasResidual = this.HasResidual;
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
LinearSegment segment = enumerator.Current;
|
||||
PointF p0 = segment.Start;
|
||||
PointF p1 = segment.End;
|
||||
if (hasResidual)
|
||||
{
|
||||
p0 = PointF.Transform(p0, residual);
|
||||
p1 = PointF.Transform(p1, residual);
|
||||
}
|
||||
|
||||
this.AddUncontainedLine(
|
||||
((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX,
|
||||
((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY,
|
||||
((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX,
|
||||
((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clips one geometry line against the destination interest and adds the retained result.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate in translated float space.</param>
|
||||
/// <param name="y0">The starting Y coordinate in translated float space.</param>
|
||||
/// <param name="x1">The ending X coordinate in translated float space.</param>
|
||||
/// <param name="y1">The ending Y coordinate in translated float space.</param>
|
||||
protected void AddUncontainedLine(float x0, float y0, float x1, float y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (y0 <= 0F && y1 <= 0F)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (y0 >= this.Height && y1 >= this.Height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (x0 >= this.Width && x1 >= this.Width)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (x0 == x1)
|
||||
{
|
||||
int x0c = Math.Clamp(FloatToFixed24Dot8(x0), 0, this.Width * FixedOne);
|
||||
int p0y = Math.Clamp(FloatToFixed24Dot8(y0), 0, this.Height * FixedOne);
|
||||
int p1y = Math.Clamp(FloatToFixed24Dot8(y1), 0, this.Height * FixedOne);
|
||||
|
||||
if (x0c == 0)
|
||||
{
|
||||
// Segments clipped fully to the left edge do not produce a visible line, but they
|
||||
// still change winding for rows they cross. Retain that effect as start covers.
|
||||
this.UpdateStartCoversClipped(p0y, p1y);
|
||||
this.hasAnyCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddContainedLineF24Dot8(x0c, p0y, x0c, p1y);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
double deltayV = Math.Abs(y1 - y0);
|
||||
double deltaxV = x1 - x0;
|
||||
double rx0 = x0;
|
||||
double ry0 = y0;
|
||||
double rx1 = x1;
|
||||
double ry1 = y1;
|
||||
|
||||
if (y1 > y0)
|
||||
{
|
||||
if (y0 < 0F)
|
||||
{
|
||||
double t = -y0 / deltayV;
|
||||
rx0 = x0 + (deltaxV * t);
|
||||
ry0 = 0D;
|
||||
}
|
||||
|
||||
if (y1 > this.Height)
|
||||
{
|
||||
double t = (this.Height - y0) / deltayV;
|
||||
rx1 = x0 + (deltaxV * t);
|
||||
ry1 = this.Height;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (y0 > this.Height)
|
||||
{
|
||||
double t = (y0 - this.Height) / deltayV;
|
||||
rx0 = x0 + (deltaxV * t);
|
||||
ry0 = this.Height;
|
||||
}
|
||||
|
||||
if (y1 < 0F)
|
||||
{
|
||||
double t = y0 / deltayV;
|
||||
rx1 = x0 + (deltaxV * t);
|
||||
ry1 = 0D;
|
||||
}
|
||||
}
|
||||
|
||||
if (rx0 >= this.Width && rx1 >= this.Width)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (rx0 > 0D && rx1 > 0D && rx0 < this.Width && rx1 < this.Width)
|
||||
{
|
||||
this.AddContainedLineF24Dot8(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rx0 <= 0D && rx1 <= 0D)
|
||||
{
|
||||
// A segment that stays left of the visible band contributes winding only.
|
||||
this.UpdateStartCoversClipped(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne));
|
||||
this.hasAnyCoverage = true;
|
||||
return;
|
||||
}
|
||||
|
||||
double deltayH = ry1 - ry0;
|
||||
double deltaxH = Math.Abs(rx1 - rx0);
|
||||
|
||||
if (rx1 > rx0)
|
||||
{
|
||||
double bx1 = rx1;
|
||||
double by1 = ry1;
|
||||
|
||||
if (rx1 > this.Width)
|
||||
{
|
||||
double t = (this.Width - rx0) / deltaxH;
|
||||
by1 = ry0 + (deltayH * t);
|
||||
bx1 = this.Width;
|
||||
}
|
||||
|
||||
if (rx0 < 0D)
|
||||
{
|
||||
double t = -rx0 / deltaxH;
|
||||
int a = Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne);
|
||||
int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne);
|
||||
int cx = Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne);
|
||||
int cy = Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne);
|
||||
|
||||
this.UpdateStartCoversClipped(a, by);
|
||||
this.hasAnyCoverage = true;
|
||||
|
||||
// The visible portion begins exactly at x == 0 after the left-edge clip.
|
||||
this.AddContainedLineF24Dot8(0, by, cx, cy);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddContainedLineF24Dot8(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
double bx0 = rx0;
|
||||
double by0 = ry0;
|
||||
|
||||
if (rx0 > this.Width)
|
||||
{
|
||||
double t = (rx0 - this.Width) / deltaxH;
|
||||
by0 = ry0 + (deltayH * t);
|
||||
bx0 = this.Width;
|
||||
}
|
||||
|
||||
if (rx1 < 0D)
|
||||
{
|
||||
double t = rx0 / deltaxH;
|
||||
int ax = Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne);
|
||||
int ay = Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne);
|
||||
int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne);
|
||||
int c = Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne);
|
||||
|
||||
// The right-to-left case mirrors the left-edge handling above: emit the
|
||||
// visible portion first, then retain the winding-only tail as start covers.
|
||||
this.AddContainedLineF24Dot8(ax, ay, 0, by);
|
||||
this.UpdateStartCoversClipped(by, c);
|
||||
this.hasAnyCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddContainedLineF24Dot8(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one fully-contained line segment in 24.8 fixed-point coordinates.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate.</param>
|
||||
/// <param name="y0">The starting Y coordinate.</param>
|
||||
/// <param name="x1">The ending X coordinate.</param>
|
||||
/// <param name="y1">The ending Y coordinate.</param>
|
||||
protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (x0 == x1)
|
||||
{
|
||||
if (y0 < y1)
|
||||
{
|
||||
this.VerticalDown(x0, y0, y1);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.VerticalUp(x0, y0, y1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int dx = Math.Abs(x1 - x0);
|
||||
int dy = Math.Abs(y1 - y0);
|
||||
if (dx > MaximumDelta || dy > MaximumDelta)
|
||||
{
|
||||
int mx = (x0 + x1) >> 1;
|
||||
int my = (y0 + y1) >> 1;
|
||||
this.AddContainedLineF24Dot8(x0, y0, mx, my);
|
||||
this.AddContainedLineF24Dot8(mx, my, x1, y1);
|
||||
return;
|
||||
}
|
||||
|
||||
int rowIndex0;
|
||||
int rowIndex1;
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
if (y0 < y1)
|
||||
{
|
||||
rowIndex0 = (y0 - bandTopStart) / bandHeight;
|
||||
rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight;
|
||||
rowIndex1 = (y1 - bandTopStart) / bandHeight;
|
||||
}
|
||||
|
||||
if ((uint)rowIndex0 >= (uint)this.RowBandCount || (uint)rowIndex1 >= (uint)this.RowBandCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (rowIndex0 == rowIndex1)
|
||||
{
|
||||
int rowTop = bandTopStart + (rowIndex0 * bandHeight);
|
||||
this.AppendLine(rowIndex0, x0, y0 - rowTop, x1, y1 - rowTop);
|
||||
this.LineCounts[rowIndex0]++;
|
||||
this.hasAnyCoverage = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.SplitAcrossBands(x0, y0, x1, y1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the mutable line collector used for one row band.
|
||||
/// </summary>
|
||||
/// <returns>The mutable line collector.</returns>
|
||||
protected abstract TL CreateLineArray();
|
||||
|
||||
/// <summary>
|
||||
/// Appends one line segment into the retained row-band collector.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">The local row-band index.</param>
|
||||
/// <param name="x0">The starting X coordinate relative to the row band.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="x1">The ending X coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
protected abstract void AppendLine(int rowIndex, int x0, int y0, int x1, int y1);
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes the mutable collectors into the retained line-block representation.
|
||||
/// </summary>
|
||||
protected abstract void FinalizeLines();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mutable line collector for a row band, creating it on first use.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">The local row-band index.</param>
|
||||
/// <returns>The mutable line collector.</returns>
|
||||
protected TL GetOrCreateLineArray(int rowIndex)
|
||||
{
|
||||
TL? lineArray = this.LineArrays[rowIndex];
|
||||
if (lineArray is not null)
|
||||
{
|
||||
return lineArray;
|
||||
}
|
||||
|
||||
lineArray = this.CreateLineArray();
|
||||
this.LineArrays[rowIndex] = lineArray;
|
||||
return lineArray;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a downward vertical segment by delegating to the shared band-splitting path.
|
||||
/// </summary>
|
||||
/// <param name="x">The fixed-point X coordinate.</param>
|
||||
/// <param name="y0">The starting fixed-point Y coordinate.</param>
|
||||
/// <param name="y1">The ending fixed-point Y coordinate.</param>
|
||||
private void VerticalDown(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an upward vertical segment by delegating to the shared band-splitting path.
|
||||
/// </summary>
|
||||
/// <param name="x">The fixed-point X coordinate.</param>
|
||||
/// <param name="y0">The starting fixed-point Y coordinate.</param>
|
||||
/// <param name="y1">The ending fixed-point Y coordinate.</param>
|
||||
private void VerticalUp(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1);
|
||||
|
||||
/// <summary>
|
||||
/// Splits a contained line segment at row-band boundaries and appends each retained piece.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate.</param>
|
||||
/// <param name="y0">The starting Y coordinate.</param>
|
||||
/// <param name="x1">The ending X coordinate.</param>
|
||||
/// <param name="y1">The ending Y coordinate.</param>
|
||||
private void SplitAcrossBands(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
int dy = y1 - y0;
|
||||
int dx = x1 - x0;
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
int startBand = dy > 0 ? (y0 - bandTopStart) / bandHeight : ((y0 - 1) - bandTopStart) / bandHeight;
|
||||
int endBand = dy > 0 ? ((y1 - 1) - bandTopStart) / bandHeight : (y1 - bandTopStart) / bandHeight;
|
||||
int step = dy > 0 ? 1 : -1;
|
||||
int currentBand = startBand;
|
||||
int currentX = x0;
|
||||
int currentY = y0;
|
||||
|
||||
while (currentBand != endBand)
|
||||
{
|
||||
int bandBoundaryY = dy > 0 ? bandTopStart + ((currentBand + 1) * bandHeight) : bandTopStart + (currentBand * bandHeight);
|
||||
int deltaY = bandBoundaryY - currentY;
|
||||
int nextX = currentX + (int)(((long)dx * deltaY) / dy);
|
||||
int rowTop = bandTopStart + (currentBand * bandHeight);
|
||||
|
||||
// Each retained segment is stored in the local coordinate space of its owning band.
|
||||
this.AppendLine(currentBand, currentX, currentY - rowTop, nextX, bandBoundaryY - rowTop);
|
||||
this.LineCounts[currentBand]++;
|
||||
this.hasAnyCoverage = true;
|
||||
currentX = nextX;
|
||||
currentY = bandBoundaryY;
|
||||
currentBand += step;
|
||||
|
||||
if ((uint)currentBand >= (uint)this.RowBandCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int finalRowTop = bandTopStart + (endBand * bandHeight);
|
||||
this.AppendLine(endBand, currentX, currentY - finalRowTop, x1, y1 - finalRowTop);
|
||||
this.LineCounts[endBand]++;
|
||||
this.hasAnyCoverage = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates retained start-cover rows for a line that has been clipped against the visible band.
|
||||
/// </summary>
|
||||
/// <param name="y0">The clipped starting Y coordinate.</param>
|
||||
/// <param name="y1">The clipped ending Y coordinate.</param>
|
||||
private void UpdateStartCoversClipped(int y0, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (y0 < y1)
|
||||
{
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
int rowIndex0 = (y0 - bandTopStart) / bandHeight;
|
||||
int rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight;
|
||||
rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1);
|
||||
rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1);
|
||||
int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight));
|
||||
int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight));
|
||||
this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : bandHeight);
|
||||
for (int i = rowIndex0 + 1; i < rowIndex1; i++)
|
||||
{
|
||||
// Full interior bands receive a constant winding contribution.
|
||||
this.FillStartCovers(i, -FixedOne);
|
||||
}
|
||||
|
||||
if (rowIndex0 != rowIndex1)
|
||||
{
|
||||
this.UpdateStartCovers(rowIndex1, 0, fy1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
int rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight;
|
||||
int rowIndex1 = (y1 - bandTopStart) / bandHeight;
|
||||
rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1);
|
||||
rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1);
|
||||
int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight));
|
||||
int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight));
|
||||
this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : 0);
|
||||
for (int i = rowIndex0 - 1; i > rowIndex1; i--)
|
||||
{
|
||||
// Full interior bands receive a constant winding contribution.
|
||||
this.FillStartCovers(i, FixedOne);
|
||||
}
|
||||
|
||||
if (rowIndex0 != rowIndex1)
|
||||
{
|
||||
this.UpdateStartCovers(rowIndex1, bandHeight, fy1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills an entire retained start-cover row with a constant winding value.
|
||||
/// </summary>
|
||||
/// <param name="localBandIndex">The local row-band index.</param>
|
||||
/// <param name="value">The constant winding value to add.</param>
|
||||
private void FillStartCovers(int localBandIndex, int value)
|
||||
{
|
||||
IMemoryOwner<int>? owner = this.StartCoverTable[localBandIndex];
|
||||
if (owner is null)
|
||||
{
|
||||
owner = this.Allocator.Allocate<int>(PreferredRowHeight, AllocationOptions.Clean);
|
||||
this.StartCoverTable[localBandIndex] = owner;
|
||||
owner.Memory.Span[..PreferredRowHeight].Fill(value);
|
||||
return;
|
||||
}
|
||||
|
||||
Span<int> covers = owner.Memory.Span[..PreferredRowHeight];
|
||||
for (int i = 0; i < PreferredRowHeight; i++)
|
||||
{
|
||||
covers[i] += value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a retained start-cover row for one clipped vertical interval.
|
||||
/// </summary>
|
||||
/// <param name="localBandIndex">The local row-band index.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
private void UpdateStartCovers(int localBandIndex, int y0, int y1)
|
||||
{
|
||||
IMemoryOwner<int>? owner = this.StartCoverTable[localBandIndex];
|
||||
if (owner is null)
|
||||
{
|
||||
owner = this.Allocator.Allocate<int>(PreferredRowHeight, AllocationOptions.Clean);
|
||||
this.StartCoverTable[localBandIndex] = owner;
|
||||
}
|
||||
|
||||
Span<int> covers = owner.Memory.Span[..PreferredRowHeight];
|
||||
if (y0 < y1)
|
||||
{
|
||||
UpdateCoverTableDown(covers, y0, y1);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateCoverTableUp(covers, y0, y1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a downward winding contribution to one retained start-cover table.
|
||||
/// </summary>
|
||||
/// <param name="covers">The retained start-cover rows.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
private static void UpdateCoverTableDown(Span<int> covers, int y0, int y1)
|
||||
{
|
||||
int rowIndex0 = y0 >> FixedShift;
|
||||
int rowIndex1 = (y1 - 1) >> FixedShift;
|
||||
int fy0 = y0 - (rowIndex0 << FixedShift);
|
||||
int fy1 = y1 - (rowIndex1 << FixedShift);
|
||||
|
||||
if (rowIndex0 == rowIndex1)
|
||||
{
|
||||
covers[rowIndex0] -= fy1 - fy0;
|
||||
return;
|
||||
}
|
||||
|
||||
covers[rowIndex0] -= FixedOne - fy0;
|
||||
for (int i = rowIndex0 + 1; i < rowIndex1; i++)
|
||||
{
|
||||
covers[i] -= FixedOne;
|
||||
}
|
||||
|
||||
covers[rowIndex1] -= fy1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies an upward winding contribution to one retained start-cover table.
|
||||
/// </summary>
|
||||
/// <param name="covers">The retained start-cover rows.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
private static void UpdateCoverTableUp(Span<int> covers, int y0, int y1)
|
||||
{
|
||||
int rowIndex0 = (y0 - 1) >> FixedShift;
|
||||
int rowIndex1 = y1 >> FixedShift;
|
||||
int fy0 = y0 - (rowIndex0 << FixedShift);
|
||||
int fy1 = y1 - (rowIndex1 << FixedShift);
|
||||
|
||||
if (rowIndex0 == rowIndex1)
|
||||
{
|
||||
covers[rowIndex0] += fy0 - fy1;
|
||||
return;
|
||||
}
|
||||
|
||||
covers[rowIndex0] += fy0;
|
||||
for (int i = rowIndex0 - 1; i > rowIndex1; i--)
|
||||
{
|
||||
covers[i] += FixedOne;
|
||||
}
|
||||
|
||||
covers[rowIndex1] += FixedOne - fy1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizer that finalizes retained lines into the 32-bit-X encoding.
|
||||
/// </summary>
|
||||
private sealed class LinearizerX32Y16 : Linearizer<LineArrayX32Y16>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearizerX32Y16"/> class.
|
||||
/// </summary>
|
||||
public LinearizerX32Y16(
|
||||
LinearGeometry geometry,
|
||||
Matrix4x4 residual,
|
||||
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.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 32-bit-X linearization pass and returns the retained result.
|
||||
/// </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>
|
||||
/// Linearizer that finalizes retained lines into the packed 16-bit-X encoding.
|
||||
/// </summary>
|
||||
private sealed class LinearizerX16Y16 : Linearizer<LineArrayX16Y16>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearizerX16Y16"/> class.
|
||||
/// </summary>
|
||||
public LinearizerX16Y16(
|
||||
LinearGeometry geometry,
|
||||
Matrix4x4 residual,
|
||||
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.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 16-bit-X linearization pass and returns the retained result.
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,174 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Flush-scoped retained row-local raster payload for one prepared fill geometry.
|
||||
/// </summary>
|
||||
internal sealed class RasterizableGeometry : IDisposable
|
||||
{
|
||||
private readonly RasterizableBandInfo[] bandInfos;
|
||||
private readonly LineArrayX16Y16Block?[]? linesX16;
|
||||
private readonly LineArrayX32Y16Block?[]? linesX32;
|
||||
private readonly int[] firstBlockLineCounts;
|
||||
private readonly IMemoryOwner<int>?[] startCoverTable;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizableGeometry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="firstRowBandIndex">The first absolute row-band index touched by the geometry.</param>
|
||||
/// <param name="rowBandCount">The number of retained local row bands owned by the geometry.</param>
|
||||
/// <param name="width">The geometry-local visible band width in pixels.</param>
|
||||
/// <param name="wordsPerRow">The bit-vector width in machine words required by the geometry.</param>
|
||||
/// <param name="coverStride">The scanner cover/area stride required by the geometry.</param>
|
||||
/// <param name="bandHeight">The retained row-band height in pixels.</param>
|
||||
/// <param name="isX16">Indicates whether the geometry uses the narrow X16Y16 line encoding.</param>
|
||||
/// <param name="bandInfos">The retained metadata for each local row band.</param>
|
||||
/// <param name="linesX16">The retained narrow line chains for each local row band.</param>
|
||||
/// <param name="linesX32">The retained wide line chains for each local row band.</param>
|
||||
/// <param name="firstBlockLineCounts">The valid line count in each front retained block.</param>
|
||||
/// <param name="startCoverTable">The retained start-cover table for each local row band.</param>
|
||||
public RasterizableGeometry(
|
||||
int firstRowBandIndex,
|
||||
int rowBandCount,
|
||||
int width,
|
||||
int wordsPerRow,
|
||||
int coverStride,
|
||||
int bandHeight,
|
||||
bool isX16,
|
||||
RasterizableBandInfo[] bandInfos,
|
||||
LineArrayX16Y16Block?[]? linesX16,
|
||||
LineArrayX32Y16Block?[]? linesX32,
|
||||
int[] firstBlockLineCounts,
|
||||
IMemoryOwner<int>?[] startCoverTable)
|
||||
{
|
||||
this.FirstRowBandIndex = firstRowBandIndex;
|
||||
this.RowBandCount = rowBandCount;
|
||||
this.Width = width;
|
||||
this.WordsPerRow = wordsPerRow;
|
||||
this.CoverStride = coverStride;
|
||||
this.BandHeight = bandHeight;
|
||||
this.IsX16 = isX16;
|
||||
this.bandInfos = bandInfos;
|
||||
this.linesX16 = linesX16;
|
||||
this.linesX32 = linesX32;
|
||||
this.firstBlockLineCounts = firstBlockLineCounts;
|
||||
this.startCoverTable = startCoverTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first absolute row-band index touched by this geometry.
|
||||
/// </summary>
|
||||
public int FirstRowBandIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of retained local row bands owned by this geometry.
|
||||
/// </summary>
|
||||
public int RowBandCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the geometry-local visible band width in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit-vector width in machine words required by this geometry.
|
||||
/// </summary>
|
||||
public int WordsPerRow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scanner cover/area stride required by this geometry.
|
||||
/// </summary>
|
||||
public int CoverStride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained row-band height in pixels.
|
||||
/// </summary>
|
||||
public int BandHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this geometry uses Blaze's narrow X16Y16 line arrays.
|
||||
/// </summary>
|
||||
public bool IsX16 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> when the given local row band has retained coverage payload.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns><see langword="true"/> when the row band has retained coverage; otherwise <see langword="false"/>.</returns>
|
||||
public bool HasCoverage(int localRowIndex) => this.bandInfos[localRowIndex].HasCoverage;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained narrow line block chain for one local row.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained narrow line chain for the row.</returns>
|
||||
public LineArrayX16Y16Block? GetLinesX16ForRow(int localRowIndex) => this.linesX16![localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained wide line block chain for one local row.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained wide line chain for the row.</returns>
|
||||
public LineArrayX32Y16Block? GetLinesX32ForRow(int localRowIndex) => this.linesX32![localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of valid lines in the first retained block for a local row.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The valid line count in the front retained block.</returns>
|
||||
public int GetFirstBlockLineCountForRow(int localRowIndex) => this.firstBlockLineCounts[localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover table entry for a local row, if one exists.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained start-cover span for the row.</returns>
|
||||
public ReadOnlySpan<int> GetCoversForRow(int localRowIndex)
|
||||
{
|
||||
IMemoryOwner<int>? covers = this.startCoverTable[localRowIndex];
|
||||
return covers is null ? ReadOnlySpan<int>.Empty : covers.Memory.Span[..this.BandHeight];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover row payload without further interpretation, matching Blaze naming.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained start-cover span for the row.</returns>
|
||||
public ReadOnlySpan<int> GetActualCoversForRow(int localRowIndex) => this.GetCoversForRow(localRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets retained metadata for one local row band.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained band metadata.</returns>
|
||||
public RasterizableBandInfo GetBandInfo(int localRowIndex) => this.bandInfos[localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Releases the retained line blocks and start-cover storage.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.linesX16 is not null)
|
||||
{
|
||||
Array.Clear(this.linesX16);
|
||||
}
|
||||
|
||||
if (this.linesX32 is not null)
|
||||
{
|
||||
Array.Clear(this.linesX32);
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.startCoverTable.Length; i++)
|
||||
{
|
||||
this.startCoverTable[i]?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,536 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// References one retained rasterizable geometry row inside a prepared scene item.
|
||||
/// </summary>
|
||||
internal readonly struct RasterizableItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizableItem"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="rasterizable">The retained rasterizable geometry.</param>
|
||||
/// <param name="localRowIndex">The local row index within <paramref name="rasterizable"/>.</param>
|
||||
public RasterizableItem(RasterizableGeometry rasterizable, int localRowIndex)
|
||||
{
|
||||
this.Rasterizable = rasterizable;
|
||||
this.LocalRowIndex = localRowIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained rasterizable geometry.
|
||||
/// </summary>
|
||||
public RasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local row index within <see cref="Rasterizable"/>.
|
||||
/// </summary>
|
||||
public int LocalRowIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of lines stored in the first retained block for this row.
|
||||
/// </summary>
|
||||
/// <returns>The number of valid lines in the leading block.</returns>
|
||||
public int GetFirstBlockLineCount() => this.Rasterizable.GetFirstBlockLineCountForRow(this.LocalRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the 16-bit X retained line block for the row when the geometry uses the compact encoding.
|
||||
/// </summary>
|
||||
/// <returns>The retained block chain, or <see langword="null"/> when the row uses the 32-bit encoding.</returns>
|
||||
public LineArrayX16Y16Block? GetLineArrayX16() => this.Rasterizable.GetLinesX16ForRow(this.LocalRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the 32-bit X retained line block for the row when the geometry uses the wide encoding.
|
||||
/// </summary>
|
||||
/// <returns>The retained block chain, or <see langword="null"/> when the row uses the 16-bit encoding.</returns>
|
||||
public LineArrayX32Y16Block? GetLineArrayX32() => this.Rasterizable.GetLinesX32ForRow(this.LocalRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover seeds for the row.
|
||||
/// </summary>
|
||||
/// <returns>The retained start-cover span.</returns>
|
||||
public ReadOnlySpan<int> GetActualCovers() => this.Rasterizable.GetActualCoversForRow(this.LocalRowIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// References one retained stroke row inside a prepared scene item.
|
||||
/// </summary>
|
||||
internal readonly struct StrokeRasterizableItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokeRasterizableItem"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="rasterizable">The retained stroke rasterizable geometry.</param>
|
||||
/// <param name="localRowIndex">The local row index within <paramref name="rasterizable"/>.</param>
|
||||
public StrokeRasterizableItem(StrokeRasterizableGeometry rasterizable, int localRowIndex)
|
||||
{
|
||||
this.Rasterizable = rasterizable;
|
||||
this.LocalRowIndex = localRowIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained stroke rasterizable geometry.
|
||||
/// </summary>
|
||||
public StrokeRasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local row index within <see cref="Rasterizable"/>.
|
||||
/// </summary>
|
||||
public int LocalRowIndex { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata that describes one prepared rasterizable band.
|
||||
/// </summary>
|
||||
internal readonly struct RasterizableBandInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizableBandInfo"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="lineCount">The number of retained visible lines in the band.</param>
|
||||
/// <param name="bandHeight">The band height in pixels.</param>
|
||||
/// <param name="width">The visible band width in pixels.</param>
|
||||
/// <param name="wordsPerRow">The bit-vector width in machine words.</param>
|
||||
/// <param name="coverStride">The scanner cover/area stride.</param>
|
||||
/// <param name="destinationLeft">The absolute destination X coordinate of the band's left column.</param>
|
||||
/// <param name="destinationTop">The absolute destination Y coordinate of the band's top row.</param>
|
||||
/// <param name="intersectionRule">The fill rule used when resolving accumulated winding.</param>
|
||||
/// <param name="rasterizationMode">The rasterization mode used by the band.</param>
|
||||
/// <param name="antialiasThreshold">The aliased threshold used when the band runs in aliased mode.</param>
|
||||
/// <param name="hasStartCovers">Indicates whether the band has non-zero start-cover seeds.</param>
|
||||
public RasterizableBandInfo(
|
||||
int lineCount,
|
||||
int bandHeight,
|
||||
int width,
|
||||
int wordsPerRow,
|
||||
int coverStride,
|
||||
int destinationLeft,
|
||||
int destinationTop,
|
||||
IntersectionRule intersectionRule,
|
||||
RasterizationMode rasterizationMode,
|
||||
float antialiasThreshold,
|
||||
bool hasStartCovers)
|
||||
{
|
||||
this.LineCount = lineCount;
|
||||
this.BandHeight = bandHeight;
|
||||
this.Width = width;
|
||||
this.WordsPerRow = wordsPerRow;
|
||||
this.CoverStride = coverStride;
|
||||
this.DestinationLeft = destinationLeft;
|
||||
this.DestinationTop = destinationTop;
|
||||
this.IntersectionRule = intersectionRule;
|
||||
this.RasterizationMode = rasterizationMode;
|
||||
this.AntialiasThreshold = antialiasThreshold;
|
||||
this.HasStartCovers = hasStartCovers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of visible raster lines stored for the band.
|
||||
/// </summary>
|
||||
public int LineCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the band height in pixels.
|
||||
/// </summary>
|
||||
public int BandHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible band width in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit-vector width in machine words.
|
||||
/// </summary>
|
||||
public int WordsPerRow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scanner cover/area stride.
|
||||
/// </summary>
|
||||
public int CoverStride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination X coordinate of the band's left column.
|
||||
/// </summary>
|
||||
public int DestinationLeft { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination Y coordinate of the band's top row.
|
||||
/// </summary>
|
||||
public int DestinationTop { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fill rule used when resolving accumulated winding.
|
||||
/// </summary>
|
||||
public IntersectionRule IntersectionRule { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the coverage mode used by the band.
|
||||
/// </summary>
|
||||
public RasterizationMode RasterizationMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the aliased threshold used when the band runs in aliased mode.
|
||||
/// </summary>
|
||||
public float AntialiasThreshold { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the band has non-zero start-cover seeds.
|
||||
/// </summary>
|
||||
public bool HasStartCovers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the band would emit any coverage.
|
||||
/// </summary>
|
||||
public bool HasCoverage => this.LineCount > 0 || this.HasStartCovers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects retained line segments whose X coordinates require 32-bit storage.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX32Y16
|
||||
{
|
||||
private LineArrayX32Y16Block? current;
|
||||
private int count = LineArrayX32Y16Block.LineCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the front block in the retained line chain.
|
||||
/// </summary>
|
||||
/// <returns>The front retained block, or <see langword="null"/> when no lines were appended.</returns>
|
||||
public LineArrayX32Y16Block? GetFrontBlock() => this.current;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of valid lines in the front retained block.
|
||||
/// </summary>
|
||||
/// <returns>The number of valid front-block lines.</returns>
|
||||
public int GetFrontBlockLineCount() => this.current is null ? 0 : this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Appends one retained line to the chain.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y0">The starting Y coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="x1">The ending X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y1">The ending Y coordinate in 24.8 fixed-point.</param>
|
||||
public void AppendLine(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int packedY0Y1 = Pack(y0, y1);
|
||||
LineArrayX32Y16Block? block = this.current;
|
||||
int currentCount = this.count;
|
||||
if (currentCount < LineArrayX32Y16Block.LineCount)
|
||||
{
|
||||
block!.Set(currentCount, packedY0Y1, x0, x1);
|
||||
this.count = currentCount + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
LineArrayX32Y16Block next = new(block);
|
||||
next.Set(0, packedY0Y1, x0, x1);
|
||||
this.current = next;
|
||||
this.count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs two signed 16-bit fixed-point values into one 32-bit integer.
|
||||
/// </summary>
|
||||
/// <param name="lo">The low 16-bit value.</param>
|
||||
/// <param name="hi">The high 16-bit value.</param>
|
||||
/// <returns>The packed value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int Pack(int lo, int hi) => (lo & 0xFFFF) | (hi << 16);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one retained 32-bit-X line block.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX32Y16Block : ILineBlock<LineArrayX32Y16Block>
|
||||
{
|
||||
private const int BlockLineCount = 32;
|
||||
private PackedLineX32Y16Buffer lines;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineArrayX32Y16Block"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The next block in the retained chain.</param>
|
||||
public LineArrayX32Y16Block(LineArrayX32Y16Block? next) => this.Next = next;
|
||||
|
||||
/// <inheritdoc />
|
||||
public static int LineCount => BlockLineCount;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LineArrayX32Y16Block? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores one retained line into the block.
|
||||
/// </summary>
|
||||
/// <param name="index">The block-local line index.</param>
|
||||
/// <param name="packedY0Y1">The packed 16-bit Y endpoints.</param>
|
||||
/// <param name="x0">The starting X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="x1">The ending X coordinate in 24.8 fixed-point.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Set(int index, int packedY0Y1, int x0, int x1)
|
||||
{
|
||||
ref PackedLineX32Y16 line = ref this.lines[index];
|
||||
line.PackedY0Y1 = packedY0Y1;
|
||||
line.X0 = x0;
|
||||
line.X1 = x1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Rasterize(int count, ref Context context)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
PackedLineX32Y16 line = this.lines[i];
|
||||
context.RasterizeLineSegment(line.X0, UnpackLo(line.PackedY0Y1), line.X1, UnpackHi(line.PackedY0Y1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the retained block chain and rasterizes each block in sequence.
|
||||
/// </summary>
|
||||
/// <param name="firstBlockLineCount">The number of valid lines stored in the front block.</param>
|
||||
/// <param name="context">The mutable scan-conversion context.</param>
|
||||
public void Iterate(int firstBlockLineCount, ref Context context)
|
||||
{
|
||||
int count = firstBlockLineCount;
|
||||
LineArrayX32Y16Block? lineBlock = this;
|
||||
while (lineBlock is not null)
|
||||
{
|
||||
lineBlock.Rasterize(count, ref context);
|
||||
lineBlock = lineBlock.Next;
|
||||
count = LineCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the low signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked low value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackLo(int packed) => (short)(packed & 0xFFFF);
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the high signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked high value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackHi(int packed) => packed >> 16;
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained 32-bit-X line record in block-local storage.
|
||||
/// </summary>
|
||||
private struct PackedLineX32Y16
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the packed Y endpoints.
|
||||
/// </summary>
|
||||
public int PackedY0Y1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting X coordinate.
|
||||
/// </summary>
|
||||
public int X0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ending X coordinate.
|
||||
/// </summary>
|
||||
public int X1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the fixed-capacity retained line payload inline with the block object.
|
||||
/// </summary>
|
||||
[InlineArray(BlockLineCount)]
|
||||
private struct PackedLineX32Y16Buffer
|
||||
{
|
||||
private PackedLineX32Y16 element0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects retained line segments whose X coordinates fit in packed 16-bit storage.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX16Y16
|
||||
{
|
||||
private LineArrayX16Y16Block? current;
|
||||
private int count = LineArrayX16Y16Block.LineCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the front block in the retained line chain.
|
||||
/// </summary>
|
||||
/// <returns>The front retained block, or <see langword="null"/> when no lines were appended.</returns>
|
||||
public LineArrayX16Y16Block? GetFrontBlock() => this.current;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of valid lines in the front retained block.
|
||||
/// </summary>
|
||||
/// <returns>The number of valid front-block lines.</returns>
|
||||
public int GetFrontBlockLineCount() => this.current is null ? 0 : this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Appends one retained line to the chain.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y0">The starting Y coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="x1">The ending X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y1">The ending Y coordinate in 24.8 fixed-point.</param>
|
||||
public void AppendLine(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int packedY0Y1 = Pack(y0, y1);
|
||||
int packedX0X1 = Pack(x0, x1);
|
||||
LineArrayX16Y16Block? block = this.current;
|
||||
int currentCount = this.count;
|
||||
if (currentCount < LineArrayX16Y16Block.LineCount)
|
||||
{
|
||||
block!.Set(currentCount, packedY0Y1, packedX0X1);
|
||||
this.count = currentCount + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
LineArrayX16Y16Block next = new(block);
|
||||
next.Set(0, packedY0Y1, packedX0X1);
|
||||
this.current = next;
|
||||
this.count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs two signed 16-bit fixed-point values into one 32-bit integer.
|
||||
/// </summary>
|
||||
/// <param name="lo">The low 16-bit value.</param>
|
||||
/// <param name="hi">The high 16-bit value.</param>
|
||||
/// <returns>The packed value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int Pack(int lo, int hi) => (lo & 0xFFFF) | (hi << 16);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one retained 16-bit-X line block.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX16Y16Block : ILineBlock<LineArrayX16Y16Block>
|
||||
{
|
||||
private const int BlockLineCount = 32;
|
||||
private PackedLineX16Y16Buffer lines;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineArrayX16Y16Block"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The next block in the retained chain.</param>
|
||||
public LineArrayX16Y16Block(LineArrayX16Y16Block? next) => this.Next = next;
|
||||
|
||||
/// <inheritdoc />
|
||||
public static int LineCount => BlockLineCount;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LineArrayX16Y16Block? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores one retained line into the block.
|
||||
/// </summary>
|
||||
/// <param name="index">The block-local line index.</param>
|
||||
/// <param name="packedY0Y1">The packed 16-bit Y endpoints.</param>
|
||||
/// <param name="packedX0X1">The packed 16-bit X endpoints.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Set(int index, int packedY0Y1, int packedX0X1)
|
||||
{
|
||||
ref PackedLineX16Y16 line = ref this.lines[index];
|
||||
line.PackedY0Y1 = packedY0Y1;
|
||||
line.PackedX0X1 = packedX0X1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Rasterize(int count, ref Context context)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
PackedLineX16Y16 line = this.lines[i];
|
||||
context.RasterizeLineSegment(
|
||||
UnpackLo(line.PackedX0X1),
|
||||
UnpackLo(line.PackedY0Y1),
|
||||
UnpackHi(line.PackedX0X1),
|
||||
UnpackHi(line.PackedY0Y1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the retained block chain and rasterizes each block in sequence.
|
||||
/// </summary>
|
||||
/// <param name="firstBlockLineCount">The number of valid lines stored in the front block.</param>
|
||||
/// <param name="context">The mutable scan-conversion context.</param>
|
||||
public void Iterate(int firstBlockLineCount, ref Context context)
|
||||
{
|
||||
int count = firstBlockLineCount;
|
||||
LineArrayX16Y16Block? lineBlock = this;
|
||||
while (lineBlock is not null)
|
||||
{
|
||||
lineBlock.Rasterize(count, ref context);
|
||||
lineBlock = lineBlock.Next;
|
||||
count = LineCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the low signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked low value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackLo(int packed) => (short)(packed & 0xFFFF);
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the high signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked high value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackHi(int packed) => packed >> 16;
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained 16-bit-X line record in block-local storage.
|
||||
/// </summary>
|
||||
private struct PackedLineX16Y16
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the packed Y endpoints.
|
||||
/// </summary>
|
||||
public int PackedY0Y1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the packed X endpoints.
|
||||
/// </summary>
|
||||
public int PackedX0X1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the fixed-capacity retained line payload inline with the block object.
|
||||
/// </summary>
|
||||
[InlineArray(BlockLineCount)]
|
||||
private struct PackedLineX16Y16Buffer
|
||||
{
|
||||
private PackedLineX16Y16 element0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1579
ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs
Normal file
1579
ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1766
ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs
Normal file
1766
ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,71 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Base type for retained drawing backend scenes.
|
||||
/// </summary>
|
||||
public abstract class DrawingBackendScene : IDisposable
|
||||
{
|
||||
private readonly IReadOnlyList<IDisposable>? ownedResources;
|
||||
private bool isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingBackendScene"/> class.
|
||||
/// </summary>
|
||||
/// <param name="bounds">The target bounds used to create the scene.</param>
|
||||
/// <param name="ownedResources">Resources that must stay alive for the retained scene.</param>
|
||||
protected DrawingBackendScene(
|
||||
Rectangle bounds,
|
||||
IReadOnlyList<IDisposable>? ownedResources)
|
||||
{
|
||||
this.Bounds = bounds;
|
||||
this.ownedResources = ownedResources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target bounds used to create the scene.
|
||||
/// </summary>
|
||||
public Rectangle Bounds { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.DisposeCore();
|
||||
this.DisposeOwnedResources();
|
||||
this.isDisposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes backend-specific resources retained by this scene.
|
||||
/// </summary>
|
||||
protected virtual void DisposeCore()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources retained for image-brush commands in this scene.
|
||||
/// </summary>
|
||||
private void DisposeOwnedResources()
|
||||
{
|
||||
if (this.ownedResources is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.ownedResources.Count; i++)
|
||||
{
|
||||
this.ownedResources[i].Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One prepared draw-order command batch consumed by a drawing backend.
|
||||
/// </summary>
|
||||
public readonly struct DrawingCommandBatch
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCommandBatch"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="commands">The draw-order scene commands.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command stream contains layer boundaries.</param>
|
||||
public DrawingCommandBatch(
|
||||
IReadOnlyList<CompositionSceneCommand> commands,
|
||||
bool hasLayers)
|
||||
{
|
||||
this.Commands = commands;
|
||||
this.HasLayers = hasLayers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCommandBatch"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="commands">The backing command buffer.</param>
|
||||
/// <param name="commandCount">The number of commands in the prepared batch.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command stream contains layer boundaries.</param>
|
||||
internal DrawingCommandBatch(
|
||||
CompositionSceneCommand[] commands,
|
||||
int commandCount,
|
||||
bool hasLayers)
|
||||
: this(new ArraySegment<CompositionSceneCommand>(commands, 0, commandCount), hasLayers)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCommandBatch"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="commands">The backing command buffer.</param>
|
||||
/// <param name="startIndex">The first command index.</param>
|
||||
/// <param name="commandCount">The number of commands in the prepared batch.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command stream contains layer boundaries.</param>
|
||||
internal DrawingCommandBatch(
|
||||
CompositionSceneCommand[] commands,
|
||||
int startIndex,
|
||||
int commandCount,
|
||||
bool hasLayers)
|
||||
: this(new ArraySegment<CompositionSceneCommand>(commands, startIndex, commandCount), hasLayers)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the draw-order scene commands.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CompositionSceneCommand> Commands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of draw-order commands in the scene.
|
||||
/// </summary>
|
||||
public int CommandCount => this.Commands.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this scene contains inline layer commands.
|
||||
/// </summary>
|
||||
public bool HasLayers { get; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,475 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Represents a flush-ready CPU scene built from retained row-local raster payload.
|
||||
/// </summary>
|
||||
internal sealed partial class FlushScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the retained row operation carried by a <see cref="SceneOperation"/>.
|
||||
/// </summary>
|
||||
internal enum SceneOperationKind : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// A retained fill item.
|
||||
/// </summary>
|
||||
FillItem = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A retained stroke item.
|
||||
/// </summary>
|
||||
StrokeItem = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Starts an isolated compositing layer.
|
||||
/// </summary>
|
||||
BeginLayer = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Ends the most recently opened layer.
|
||||
/// </summary>
|
||||
EndLayer = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained row operation.
|
||||
/// </summary>
|
||||
internal readonly struct SceneOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneOperation"/> struct for a draw item.
|
||||
/// </summary>
|
||||
/// <param name="kind">The retained draw operation kind.</param>
|
||||
/// <param name="itemIndex">The retained scene item index.</param>
|
||||
/// <param name="localRowIndex">The retained rasterizable row index.</param>
|
||||
public SceneOperation(SceneOperationKind kind, int itemIndex, int localRowIndex)
|
||||
{
|
||||
this.Kind = kind;
|
||||
this.ItemIndex = itemIndex;
|
||||
this.LocalRowIndex = localRowIndex;
|
||||
this.LayerBounds = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneOperation"/> struct for a layer control operation.
|
||||
/// </summary>
|
||||
/// <param name="kind">The layer operation kind.</param>
|
||||
/// <param name="layerBounds">The retained row-local layer bounds.</param>
|
||||
/// <param name="itemIndex">The retained layer-options index for begin-layer operations.</param>
|
||||
public SceneOperation(CompositionCommandKind kind, Rectangle layerBounds, int itemIndex)
|
||||
{
|
||||
this.Kind = kind == CompositionCommandKind.BeginLayer ? SceneOperationKind.BeginLayer : SceneOperationKind.EndLayer;
|
||||
this.ItemIndex = itemIndex;
|
||||
this.LocalRowIndex = -1;
|
||||
this.LayerBounds = layerBounds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operation kind.
|
||||
/// </summary>
|
||||
public SceneOperationKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained scene item index for fill operations.
|
||||
/// </summary>
|
||||
public int ItemIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained rasterizable row index for fill operations.
|
||||
/// </summary>
|
||||
public int LocalRowIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained row-local layer bounds for layer operations.
|
||||
/// </summary>
|
||||
public Rectangle LayerBounds { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained scene row.
|
||||
/// </summary>
|
||||
internal readonly struct SceneRow : IDisposable
|
||||
{
|
||||
private readonly SceneOperationBlock? firstBlock;
|
||||
private readonly SceneOperationBlock? lastBlock;
|
||||
private readonly int rowBandIndex;
|
||||
private readonly int count;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneRow"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="firstBlock">The first retained row-item block.</param>
|
||||
/// <param name="lastBlock">The last retained row-item block.</param>
|
||||
/// <param name="rowBandIndex">The absolute row-band index represented by the row.</param>
|
||||
/// <param name="count">The number of retained operations in the row.</param>
|
||||
public SceneRow(SceneOperationBlock? firstBlock, SceneOperationBlock? lastBlock, int rowBandIndex, int count)
|
||||
{
|
||||
this.firstBlock = firstBlock;
|
||||
this.lastBlock = lastBlock;
|
||||
this.rowBandIndex = rowBandIndex;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute row-band index represented by this scene row.
|
||||
/// </summary>
|
||||
public int RowBandIndex => this.rowBandIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of row items in this scene row.
|
||||
/// </summary>
|
||||
public int Count => this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first retained row-item block.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? FirstBlock => this.firstBlock;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last retained row-item block.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? LastBlock => this.lastBlock;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the row storage.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
SceneOperationBlock? block = this.firstBlock;
|
||||
while (block is not null)
|
||||
{
|
||||
SceneOperationBlock? next = block.Next;
|
||||
block.Dispose();
|
||||
block = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends row items directly into allocator-backed row storage.
|
||||
/// </summary>
|
||||
private struct RowBuilder : IDisposable
|
||||
{
|
||||
private readonly MemoryAllocator allocator;
|
||||
private SceneOperationBlock? firstBlock;
|
||||
private SceneOperationBlock? lastBlock;
|
||||
private int count;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RowBuilder"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="allocator">The allocator used for row-block storage.</param>
|
||||
public RowBuilder(MemoryAllocator allocator)
|
||||
{
|
||||
this.allocator = allocator;
|
||||
this.firstBlock = null;
|
||||
this.lastBlock = null;
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the builder has been initialized.
|
||||
/// </summary>
|
||||
public readonly bool IsInitialized => this.allocator is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of operations appended to this builder.
|
||||
/// </summary>
|
||||
public readonly int Count => this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a row item.
|
||||
/// </summary>
|
||||
/// <param name="operation">The retained operation to append.</param>
|
||||
public void Append(SceneOperation operation)
|
||||
{
|
||||
if (this.lastBlock is null)
|
||||
{
|
||||
SceneOperationBlock block = new(this.allocator);
|
||||
block.Append(operation);
|
||||
this.firstBlock = block;
|
||||
this.lastBlock = block;
|
||||
this.count++;
|
||||
return;
|
||||
}
|
||||
|
||||
SceneOperationBlock current = this.lastBlock;
|
||||
if (current.Count < SceneOperationBlock.ItemsPerBlock)
|
||||
{
|
||||
current.Append(operation);
|
||||
this.count++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Once a row block fills, link a fresh fixed-capacity block instead of reallocating
|
||||
// and copying existing operations. This keeps the retained row builder append-only.
|
||||
SceneOperationBlock next = new(this.allocator);
|
||||
next.Append(operation);
|
||||
current.Next = next;
|
||||
next.Previous = current;
|
||||
this.lastBlock = next;
|
||||
this.count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the retained blocks owned by <paramref name="source"/> to <paramref name="destination"/>
|
||||
/// without copying individual operations.
|
||||
/// </summary>
|
||||
/// <param name="destination">The builder receiving the appended blocks.</param>
|
||||
/// <param name="source">The builder supplying the appended blocks.</param>
|
||||
public static void AppendBuilder(ref RowBuilder destination, ref RowBuilder source)
|
||||
{
|
||||
if (source.firstBlock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (destination.firstBlock is null)
|
||||
{
|
||||
destination = source;
|
||||
source = default;
|
||||
return;
|
||||
}
|
||||
|
||||
destination.lastBlock!.Next = source.firstBlock;
|
||||
source.firstBlock.Previous = destination.lastBlock;
|
||||
destination.lastBlock = source.lastBlock;
|
||||
destination.count += source.count;
|
||||
source = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes the builder into retained scene storage.
|
||||
/// </summary>
|
||||
/// <param name="rowBandIndex">The absolute row-band index represented by the row.</param>
|
||||
/// <returns>The finalized retained row.</returns>
|
||||
public readonly SceneRow Finalize(int rowBandIndex) => new(this.firstBlock, this.lastBlock, rowBandIndex, this.count);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes unfinalized storage.
|
||||
/// </summary>
|
||||
public readonly void Dispose()
|
||||
{
|
||||
SceneOperationBlock? block = this.firstBlock;
|
||||
while (block is not null)
|
||||
{
|
||||
SceneOperationBlock? next = block.Next;
|
||||
block.Dispose();
|
||||
block = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one fixed-capacity row-item block.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This mirrors Blaze's <c>RowItemList<T>::Block</c> shape: append into the current block,
|
||||
/// allocate a fresh block only when that block fills, and never reallocate or copy existing blocks.
|
||||
/// </remarks>
|
||||
internal sealed class SceneOperationBlock : IDisposable
|
||||
{
|
||||
private readonly IMemoryOwner<SceneOperation> owner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneOperationBlock"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allocator">The allocator used for block storage.</param>
|
||||
public SceneOperationBlock(MemoryAllocator allocator)
|
||||
=> this.owner = allocator.Allocate<SceneOperation>(ItemsPerBlock);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fixed item capacity per block.
|
||||
/// </summary>
|
||||
public static int ItemsPerBlock => 32;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the previous block in the row list.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? Previous { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the next block in the row list.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? Next { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of items written into this block.
|
||||
/// </summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items written into this block.
|
||||
/// </summary>
|
||||
public Span<SceneOperation> Items => this.owner.Memory.Span[..this.Count];
|
||||
|
||||
/// <summary>
|
||||
/// Appends an item into this block.
|
||||
/// </summary>
|
||||
/// <param name="operation">The retained operation to append.</param>
|
||||
public void Append(SceneOperation operation) => this.owner.Memory.Span[this.Count++] = operation;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the block storage.
|
||||
/// </summary>
|
||||
public void Dispose() => this.owner.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained fill scene item.
|
||||
/// </summary>
|
||||
internal sealed class FillSceneItem : IDisposable
|
||||
{
|
||||
private object? renderer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FillSceneItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush used by the fill item.</param>
|
||||
/// <param name="graphicsOptions">The graphics options used by the fill item.</param>
|
||||
/// <param name="brushBounds">The brush bounds used for applicator creation.</param>
|
||||
/// <param name="rasterizable">The retained rasterizable geometry.</param>
|
||||
public FillSceneItem(
|
||||
Brush brush,
|
||||
GraphicsOptions graphicsOptions,
|
||||
Rectangle brushBounds,
|
||||
DefaultRasterizer.RasterizableGeometry rasterizable)
|
||||
{
|
||||
this.Brush = brush;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
this.BrushBounds = brushBounds;
|
||||
this.Rasterizable = rasterizable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used by the fill item.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options used by the fill item.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush bounds used for applicator creation.
|
||||
/// </summary>
|
||||
public Rectangle BrushBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained rasterizable geometry.
|
||||
/// </summary>
|
||||
public DefaultRasterizer.RasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memoized renderer for this scene item, creating it on first use.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="canvasWidth">The destination canvas width.</param>
|
||||
/// <returns>The memoized renderer for the scene item.</returns>
|
||||
public BrushRenderer<TPixel> GetRenderer<TPixel>(Configuration configuration, int canvasWidth)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
if (this.renderer is BrushRenderer<TPixel> typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
typed = this.Brush.CreateRenderer<TPixel>(
|
||||
configuration,
|
||||
this.GraphicsOptions,
|
||||
canvasWidth,
|
||||
this.BrushBounds);
|
||||
|
||||
this.renderer = typed;
|
||||
return typed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.Rasterizable.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained stroke scene item.
|
||||
/// </summary>
|
||||
internal sealed class StrokeSceneItem : IDisposable
|
||||
{
|
||||
private object? renderer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokeSceneItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="brush">The prepared brush for the stroke item.</param>
|
||||
/// <param name="graphicsOptions">The graphics options for the stroke item.</param>
|
||||
/// <param name="brushBounds">The prepared brush bounds.</param>
|
||||
/// <param name="rasterizable">The retained stroke rasterizable geometry.</param>
|
||||
public StrokeSceneItem(
|
||||
Brush brush,
|
||||
GraphicsOptions graphicsOptions,
|
||||
Rectangle brushBounds,
|
||||
DefaultRasterizer.StrokeRasterizableGeometry rasterizable)
|
||||
{
|
||||
this.Brush = brush;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
this.BrushBounds = brushBounds;
|
||||
this.Rasterizable = rasterizable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prepared brush for the stroke item.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options for the stroke item.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prepared brush bounds for the stroke item.
|
||||
/// </summary>
|
||||
public Rectangle BrushBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained stroke rasterizable geometry.
|
||||
/// </summary>
|
||||
public DefaultRasterizer.StrokeRasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memoized renderer for this scene item, creating it on first use.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="canvasWidth">The destination canvas width.</param>
|
||||
/// <returns>The memoized renderer for the scene item.</returns>
|
||||
public BrushRenderer<TPixel> GetRenderer<TPixel>(Configuration configuration, int canvasWidth)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
if (this.renderer is BrushRenderer<TPixel> typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
typed = this.Brush.CreateRenderer<TPixel>(
|
||||
configuration,
|
||||
this.GraphicsOptions,
|
||||
canvasWidth,
|
||||
this.BrushBounds);
|
||||
|
||||
this.renderer = typed;
|
||||
return typed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.Rasterizable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
1168
ImageSharp.Drawing/Processing/Backends/FlushScene.cs
Normal file
1168
ImageSharp.Drawing/Processing/Backends/FlushScene.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,35 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Per-frame destination for <see cref="DrawingCanvas{TPixel}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public interface ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the frame bounds in root target coordinates.
|
||||
/// </summary>
|
||||
public Rectangle Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get a CPU-accessible destination region.
|
||||
/// </summary>
|
||||
/// <param name="region">The CPU region when available.</param>
|
||||
/// <returns><see langword="true"/> when a CPU region is available.</returns>
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get an opaque native destination surface.
|
||||
/// </summary>
|
||||
/// <param name="surface">The native surface when available.</param>
|
||||
/// <returns><see langword="true"/> when a native surface is available.</returns>
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface);
|
||||
}
|
||||
}
|
||||
57
ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs
Normal file
57
ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Defines the contract for creating and rendering retained drawing scenes for canvas targets.
|
||||
/// </summary>
|
||||
public interface IDrawingBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a retained backend scene from a prepared command batch.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="targetBounds">The target bounds used for target-dependent scene data.</param>
|
||||
/// <param name="commandBatch">The scene commands in submission order.</param>
|
||||
/// <param name="ownedResources">The resources that must stay alive for the returned scene.</param>
|
||||
/// <returns>A retained backend scene.</returns>
|
||||
public DrawingBackendScene CreateScene(
|
||||
Configuration configuration,
|
||||
Rectangle targetBounds,
|
||||
DrawingCommandBatch commandBatch,
|
||||
IReadOnlyList<IDisposable>? ownedResources = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a retained backend scene into the target.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="target">The target frame.</param>
|
||||
/// <param name="scene">The retained backend scene to render.</param>
|
||||
public void RenderScene<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
DrawingBackendScene scene)
|
||||
where TPixel : unmanaged, IPixel<TPixel>;
|
||||
|
||||
/// <summary>
|
||||
/// Reads source pixels from the target into the destination region.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="target">The target frame.</param>
|
||||
/// <param name="sourceRectangle">The source rectangle in target-local coordinates.</param>
|
||||
/// <param name="destination">The destination region that receives the copied pixels.</param>
|
||||
public void ReadRegion<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
Rectangle sourceRectangle,
|
||||
Buffer2DRegion<TPixel> destination)
|
||||
where TPixel : unmanaged, IPixel<TPixel>;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Receives one emitted non-zero coverage span from the rasterizer.
|
||||
/// </summary>
|
||||
internal interface IRasterizerCoverageRowHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles one emitted non-zero coverage span.
|
||||
/// </summary>
|
||||
/// <param name="y">The destination y coordinate.</param>
|
||||
/// <param name="startX">The first x coordinate represented by <paramref name="coverage"/>.</param>
|
||||
/// <param name="coverage">Non-zero coverage values starting at <paramref name="startX"/>.</param>
|
||||
public void Handle(int y, int startX, Span<float> coverage);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Canvas frame backed by a <see cref="Buffer2DRegion{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public sealed class MemoryCanvasFrame<TPixel> : ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly Buffer2DRegion<TPixel> region;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemoryCanvasFrame{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="region">The pixel buffer region backing this frame.</param>
|
||||
public MemoryCanvasFrame(Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
Guard.NotNull(region.Buffer, nameof(region));
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rectangle Bounds => this.region.Bounds;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
region = this.region;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface)
|
||||
{
|
||||
surface = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Canvas frame backed by a <see cref="NativeSurface"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public sealed class NativeCanvasFrame<TPixel> : ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly NativeSurface surface;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NativeCanvasFrame{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="bounds">The frame bounds.</param>
|
||||
/// <param name="surface">The native surface backing this frame.</param>
|
||||
public NativeCanvasFrame(Rectangle bounds, NativeSurface surface)
|
||||
{
|
||||
Guard.NotNull(surface, nameof(surface));
|
||||
Guard.MustBeGreaterThan(bounds.Width, 0, nameof(bounds));
|
||||
Guard.MustBeGreaterThan(bounds.Height, 0, nameof(bounds));
|
||||
|
||||
this.Bounds = bounds;
|
||||
this.surface = surface;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rectangle Bounds { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
region = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface)
|
||||
{
|
||||
surface = this.surface;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
ImageSharp.Drawing/Processing/Backends/NativeSurface.cs
Normal file
17
ImageSharp.Drawing/Processing/Backends/NativeSurface.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Base type for backend-specific native drawing targets.
|
||||
/// </summary>
|
||||
public abstract class NativeSurface
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NativeSurface"/> class.
|
||||
/// </summary>
|
||||
protected NativeSurface()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Centralizes the conversion from configuration parallelism settings to partition counts and
|
||||
/// <see cref="ParallelOptions"/> instances used by retained-scene CPU execution paths.
|
||||
/// </summary>
|
||||
internal static class ParallelExecutionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the number of partitions to schedule for work constrained by a single work-item limit.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> leaves the runtime
|
||||
/// parallelism cap unbounded, but partition planning remains capped to
|
||||
/// <see cref="Environment.ProcessorCount"/> to avoid excessive fan-out.
|
||||
/// </param>
|
||||
/// <param name="workItemCount">The total number of work items available for partitioning.</param>
|
||||
/// <returns>The number of partitions to schedule.</returns>
|
||||
public static int GetPartitionCount(int maxDegreeOfParallelism, int workItemCount)
|
||||
=> Math.Min(GetPartitionLimit(maxDegreeOfParallelism), workItemCount);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the number of partitions to schedule for work constrained by two independent limits.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> leaves the runtime
|
||||
/// parallelism cap unbounded, but partition planning remains capped to
|
||||
/// <see cref="Environment.ProcessorCount"/> to avoid excessive fan-out.
|
||||
/// </param>
|
||||
/// <param name="workItemCount">The total number of work items available for partitioning.</param>
|
||||
/// <param name="secondaryLimit">An additional caller-specific upper bound on useful partitions.</param>
|
||||
/// <returns>The number of partitions to schedule.</returns>
|
||||
public static int GetPartitionCount(int maxDegreeOfParallelism, int workItemCount, int secondaryLimit)
|
||||
=> Math.Min(GetPartitionLimit(maxDegreeOfParallelism), Math.Min(workItemCount, secondaryLimit));
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="ParallelOptions"/> for a partitioned operation.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> retains the runtime's
|
||||
/// unbounded sentinel because <paramref name="partitionCount"/> is always positive; positive
|
||||
/// values are capped to the smaller of the configured limit and the useful partition count.
|
||||
/// </param>
|
||||
/// <param name="partitionCount">The computed positive number of useful partitions for the operation.</param>
|
||||
/// <returns>The <see cref="ParallelOptions"/> instance for the operation.</returns>
|
||||
public static ParallelOptions CreateParallelOptions(int maxDegreeOfParallelism, int partitionCount)
|
||||
=> new() { MaxDegreeOfParallelism = Math.Min(maxDegreeOfParallelism, partitionCount) };
|
||||
|
||||
/// <summary>
|
||||
/// Computes the internal partition-planning cap for the configured parallelism setting.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> keeps the runtime
|
||||
/// parallelism setting unbounded, but partition planning is capped to
|
||||
/// <see cref="Environment.ProcessorCount"/>.
|
||||
/// </param>
|
||||
/// <returns>The maximum number of partitions to plan for.</returns>
|
||||
private static int GetPartitionLimit(int maxDegreeOfParallelism)
|
||||
=> maxDegreeOfParallelism == -1 ? Environment.ProcessorCount : maxDegreeOfParallelism;
|
||||
}
|
||||
}
|
||||
98
ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs
Normal file
98
ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs
Normal file
@ -0,0 +1,98 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Describes whether rasterizers should emit continuous coverage or binary aliased coverage.
|
||||
/// </summary>
|
||||
public enum RasterizationMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Emit continuous coverage in the range [0, 1].
|
||||
/// </summary>
|
||||
Antialiased = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Emit binary coverage values (0 or 1).
|
||||
/// </summary>
|
||||
Aliased = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes where sample coverage is aligned relative to destination pixels.
|
||||
/// </summary>
|
||||
public enum RasterizerSamplingOrigin
|
||||
{
|
||||
/// <summary>
|
||||
/// Samples are aligned to pixel boundaries.
|
||||
/// </summary>
|
||||
PixelBoundary = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Samples are aligned to pixel centers.
|
||||
/// </summary>
|
||||
PixelCenter = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable options used by rasterizers when scan-converting vector geometry.
|
||||
/// </summary>
|
||||
public readonly struct RasterizerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizerOptions"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="interest">Destination bounds to rasterize into.</param>
|
||||
/// <param name="intersectionRule">Polygon intersection rule.</param>
|
||||
/// <param name="rasterizationMode">Rasterization coverage mode.</param>
|
||||
/// <param name="samplingOrigin">Sampling origin alignment.</param>
|
||||
/// <param name="antialiasThreshold">Coverage threshold for aliased mode (0 to 1).</param>
|
||||
public RasterizerOptions(
|
||||
Rectangle interest,
|
||||
IntersectionRule intersectionRule,
|
||||
RasterizationMode rasterizationMode,
|
||||
RasterizerSamplingOrigin samplingOrigin,
|
||||
float antialiasThreshold)
|
||||
{
|
||||
this.Interest = interest;
|
||||
this.IntersectionRule = intersectionRule;
|
||||
this.RasterizationMode = rasterizationMode;
|
||||
this.SamplingOrigin = samplingOrigin;
|
||||
this.AntialiasThreshold = antialiasThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets destination bounds to rasterize into.
|
||||
/// </summary>
|
||||
public Rectangle Interest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the polygon intersection rule.
|
||||
/// </summary>
|
||||
public IntersectionRule IntersectionRule { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rasterization coverage mode.
|
||||
/// </summary>
|
||||
public RasterizationMode RasterizationMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sampling origin alignment.
|
||||
/// </summary>
|
||||
public RasterizerSamplingOrigin SamplingOrigin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the coverage threshold used when <see cref="RasterizationMode"/> is <see cref="RasterizationMode.Aliased"/>.
|
||||
/// Pixels with coverage above this value are rendered as fully opaque; pixels below are discarded.
|
||||
/// </summary>
|
||||
public float AntialiasThreshold { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the current options with a different interest rectangle.
|
||||
/// </summary>
|
||||
/// <param name="interest">The replacement interest rectangle.</param>
|
||||
/// <returns>A new <see cref="RasterizerOptions"/> value.</returns>
|
||||
public RasterizerOptions WithInterest(Rectangle interest)
|
||||
=> new(interest, this.IntersectionRule, this.RasterizationMode, this.SamplingOrigin, this.AntialiasThreshold);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,136 @@
|
||||
// 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 two-point line-segment command queued by the canvas batcher.
|
||||
/// </summary>
|
||||
public readonly struct StrokeLineSegmentCommand
|
||||
{
|
||||
private readonly PointF sourceStart;
|
||||
private readonly PointF sourceEnd;
|
||||
private readonly DrawingOptions drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokeLineSegmentCommand"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="sourceStart">The source line start point.</param>
|
||||
/// <param name="sourceEnd">The source line end point.</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 StrokeLineSegmentCommand(
|
||||
PointF sourceStart,
|
||||
PointF sourceEnd,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
Pen pen,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
this.sourceStart = sourceStart;
|
||||
this.sourceEnd = sourceEnd;
|
||||
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 line start point.
|
||||
/// </summary>
|
||||
public PointF SourceStart => this.sourceStart;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source line end point.
|
||||
/// </summary>
|
||||
public PointF SourceEnd => this.sourceEnd;
|
||||
|
||||
/// <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 two-point line segment.
|
||||
/// </summary>
|
||||
/// <param name="start">The line start point.</param>
|
||||
/// <param name="end">The line end point.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <returns>The conservative stroked bounds.</returns>
|
||||
public static RectangleF GetConservativeBounds(PointF start, PointF end, Pen pen)
|
||||
{
|
||||
float left = MathF.Min(start.X, end.X);
|
||||
float top = MathF.Min(start.Y, end.Y);
|
||||
float right = MathF.Max(start.X, end.X);
|
||||
float bottom = MathF.Max(start.Y, end.Y);
|
||||
RectangleF bounds = RectangleF.FromLTRB(left, top, right, bottom);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
111
ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs
Normal file
111
ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One stroked path command queued by the canvas batcher.
|
||||
/// </summary>
|
||||
public readonly struct StrokePathCommand
|
||||
{
|
||||
private readonly IPath sourcePath;
|
||||
private readonly DrawingOptions drawingOptions;
|
||||
private readonly IReadOnlyList<IPath>? clipPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokePathCommand"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">The source stroke path.</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="clipPaths">Optional clip paths supplied with the command.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
public StrokePathCommand(
|
||||
IPath sourcePath,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
Pen pen,
|
||||
IReadOnlyList<IPath>? clipPaths,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
this.sourcePath = sourcePath;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.clipPaths = clipPaths;
|
||||
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 stroke path.
|
||||
/// </summary>
|
||||
public IPath SourcePath => this.sourcePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions.Transform;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional clip paths carried by the command.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath>? ClipPaths => this.clipPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shape options carried by the command.
|
||||
/// </summary>
|
||||
public ShapeOptions ShapeOptions => this.drawingOptions.ShapeOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
}
|
||||
}
|
||||
148
ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs
Normal file
148
ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs
Normal file
@ -0,0 +1,148 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
ImageSharp.Drawing/Processing/Brush.cs
Normal file
56
ImageSharp.Drawing/Processing/Brush.cs
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Represents a logical configuration of a brush which can be used to source pixel colors.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A brush creates a <see cref="BrushRenderer{TPixel}"/> that performs the logic for retrieving
|
||||
/// pixel values for specific locations.
|
||||
/// </remarks>
|
||||
public abstract class Brush : IEquatable<Brush>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the prepared execution object for this brush.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphic options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="region">The region the brush will be applied to.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="BrushRenderer{TPixel}"/> for this brush.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// The <paramref name="region" /> when being applied to things like shapes would usually be the
|
||||
/// bounding box of the shape not necessarily the bounds of the whole image.
|
||||
/// </remarks>
|
||||
public abstract BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
where TPixel : unmanaged, IPixel<TPixel>;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new brush with its defining geometry transformed by the given matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The transformation matrix to apply.</param>
|
||||
/// <returns>A transformed brush, or <c>this</c> if the brush has no spatial parameters.</returns>
|
||||
public virtual Brush Transform(Matrix4x4 matrix) => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract bool Equals(Brush? other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => this.Equals(obj as Brush);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract override int GetHashCode();
|
||||
}
|
||||
}
|
||||
68
ImageSharp.Drawing/Processing/BrushRenderer.cs
Normal file
68
ImageSharp.Drawing/Processing/BrushRenderer.cs
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Renders a <see cref="Brush"/> against individual coverage scanlines.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public abstract class BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
protected BrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth)
|
||||
{
|
||||
this.Configuration = configuration;
|
||||
this.Options = options;
|
||||
this.CanvasWidth = canvasWidth;
|
||||
this.Blender = PixelOperations<TPixel>.Instance.GetPixelBlender(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration instance to use when performing operations.
|
||||
/// </summary>
|
||||
protected Configuration Configuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel blender.
|
||||
/// </summary>
|
||||
internal PixelBlender<TPixel> Blender { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options.
|
||||
/// </summary>
|
||||
protected GraphicsOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the canvas width for the current render pass.
|
||||
/// </summary>
|
||||
protected int CanvasWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the opacity weighting for each pixel in a scanline to the target based on the
|
||||
/// pattern contained in the brush.
|
||||
/// </summary>
|
||||
/// <param name="destinationRow">The destination row slice to shade.</param>
|
||||
/// <param name="scanline">The coverage values for the current destination scanline.</param>
|
||||
/// <param name="x">The x-position in the target pixel space that the start of the scanline data corresponds to.</param>
|
||||
/// <param name="y">The y-position in the target pixel space that the scanline corresponds to.</param>
|
||||
/// <param name="workspace">The worker-local scratch workspace for temporary blending buffers.</param>
|
||||
public abstract void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace);
|
||||
}
|
||||
}
|
||||
73
ImageSharp.Drawing/Processing/BrushWorkspace.cs
Normal file
73
ImageSharp.Drawing/Processing/BrushWorkspace.cs
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Worker-local scratch workspace used by prepared brushes during row composition.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The target pixel format.</typeparam>
|
||||
public sealed class BrushWorkspace<TPixel> : IDisposable
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly IMemoryOwner<float> amountsOwner;
|
||||
private readonly IMemoryOwner<TPixel> overlaysOwner;
|
||||
private readonly IMemoryOwner<Vector4> blendScratchOwner;
|
||||
|
||||
internal BrushWorkspace(MemoryAllocator allocator, int rowWidth)
|
||||
{
|
||||
int capacity = Math.Max(1, rowWidth);
|
||||
this.amountsOwner = allocator.Allocate<float>(capacity);
|
||||
this.overlaysOwner = allocator.Allocate<TPixel>(capacity);
|
||||
this.blendScratchOwner = allocator.Allocate<Vector4>(capacity * 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared amount buffer for the requested length.
|
||||
/// </summary>
|
||||
/// <param name="length">The number of elements required.</param>
|
||||
/// <returns>A slice of the worker-local pooled amount buffer.</returns>
|
||||
public Span<float> GetAmounts(int length)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(length);
|
||||
return this.amountsOwner.Memory.Span[..length];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared overlay buffer for the requested length.
|
||||
/// </summary>
|
||||
/// <param name="length">The number of elements required.</param>
|
||||
/// <returns>A slice of the worker-local pooled overlay buffer.</returns>
|
||||
public Span<TPixel> GetOverlays(int length)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(length);
|
||||
return this.overlaysOwner.Memory.Span[..length];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared vector scratch for the requested row length and vector row count.
|
||||
/// </summary>
|
||||
/// <param name="length">The number of pixels in the row.</param>
|
||||
/// <param name="vectorRows">The number of temporary vector rows required.</param>
|
||||
/// <returns>A slice of the worker-local pooled vector scratch buffer.</returns>
|
||||
public Span<Vector4> GetBlendScratch(int length, int vectorRows)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(length);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(vectorRows, 1);
|
||||
return this.blendScratchOwner.Memory.Span[..(length * vectorRows)];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this.amountsOwner.Dispose();
|
||||
this.overlaysOwner.Dispose();
|
||||
this.blendScratchOwner.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
648
ImageSharp.Drawing/Processing/Brushes.Hatch.cs
Normal file
648
ImageSharp.Drawing/Processing/Brushes.Hatch.cs
Normal file
@ -0,0 +1,648 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <content>
|
||||
/// Provides additional hatch pattern brush factories.
|
||||
/// </content>
|
||||
public static partial class Brushes
|
||||
{
|
||||
// These hatch arrays were derived using the GDI+ pixel extraction technique described at
|
||||
// https://web.archive.org/web/20221228174326/https://www.codeproject.com/Articles/5350583/Recreating-Gdiplus-hatches-with-SkiaSharp.
|
||||
private static readonly bool[,] HorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] VerticalPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] ForwardDiagonalPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, true, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, true, false, false, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] BackwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, true, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, false, false, },
|
||||
{ false, true, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] CrossPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DiagonalCrossPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent05Pattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent10Pattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent20Pattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent25Pattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent30Pattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent40Pattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, false, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, false, false, true, false, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent50Pattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent60Pattern =
|
||||
{
|
||||
{ true, true, true, false, true, true, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, true, true, false, true, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, true, true, false, true, true, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, true, true, false, true, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent70Pattern =
|
||||
{
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent75Pattern =
|
||||
{
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent80Pattern =
|
||||
{
|
||||
{ true, true, true, false, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, false, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent90Pattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, false, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, true, true, true, true, true, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LightDownwardDiagonalPattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LightUpwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DarkDownwardDiagonalPattern =
|
||||
{
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ false, false, true, true, false, false, true, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ false, false, true, true, false, false, true, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DarkUpwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, true, true, false, false, true, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ false, false, true, true, false, false, true, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] WideDownwardDiagonalPattern =
|
||||
{
|
||||
{ true, true, false, false, false, false, false, true, },
|
||||
{ true, true, true, false, false, false, false, false, },
|
||||
{ false, true, true, true, false, false, false, false, },
|
||||
{ false, false, true, true, true, false, false, false, },
|
||||
{ false, false, false, true, true, true, false, false, },
|
||||
{ false, false, false, false, true, true, true, false, },
|
||||
{ false, false, false, false, false, true, true, true, },
|
||||
{ true, false, false, false, false, false, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] WideUpwardDiagonalPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, true, true, },
|
||||
{ false, false, false, false, false, true, true, true, },
|
||||
{ false, false, false, false, true, true, true, false, },
|
||||
{ false, false, false, true, true, true, false, false, },
|
||||
{ false, false, true, true, true, false, false, false, },
|
||||
{ false, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, false, false, false, false, false, },
|
||||
{ true, true, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LightVerticalPattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LightHorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] NarrowVerticalPattern =
|
||||
{
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] NarrowHorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DarkVerticalPattern =
|
||||
{
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DarkHorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DashedDownwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DashedUpwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DashedHorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DashedVerticalPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SmallConfettiPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ false, false, true, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, true, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LargeConfettiPattern =
|
||||
{
|
||||
{ true, false, true, true, false, false, false, true, },
|
||||
{ false, false, true, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, true, true, },
|
||||
{ false, false, false, true, true, false, true, true, },
|
||||
{ true, true, false, true, true, false, false, false, },
|
||||
{ true, true, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, true, false, false, },
|
||||
{ true, false, false, false, true, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] ZigZagPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] WavePattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, true, false, false, true, false, true, },
|
||||
{ true, true, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, true, false, false, true, false, true, },
|
||||
{ true, true, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DiagonalBrickPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, true, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] HorizontalBrickPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] WeavePattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, true, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, true, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, true, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] PlaidPattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DivotPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DottedGridPattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DottedDiamondPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] ShinglePattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, true, true, },
|
||||
{ true, false, false, false, false, true, false, false, },
|
||||
{ false, true, false, false, true, false, false, false, },
|
||||
{ false, false, true, true, false, false, false, false, },
|
||||
{ false, false, false, false, true, true, false, false, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] TrellisPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SpherePattern =
|
||||
{
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, false, false, false, true, false, false, true, },
|
||||
{ true, false, false, false, true, true, true, true, },
|
||||
{ true, false, false, false, true, true, true, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, false, false, true, true, false, false, false, },
|
||||
{ true, true, true, true, true, false, false, false, },
|
||||
{ true, true, true, true, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SmallGridPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SmallCheckerBoardPattern =
|
||||
{
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LargeCheckerBoardPattern =
|
||||
{
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] OutlinedDiamondPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ false, false, true, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, true, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SolidDiamondPattern =
|
||||
{
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, true, true, true, false, false, false, },
|
||||
{ false, true, true, true, true, true, false, false, },
|
||||
{ true, true, true, true, true, true, true, false, },
|
||||
{ false, true, true, true, true, true, false, false, },
|
||||
{ false, false, true, true, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
}
|
||||
}
|
||||
841
ImageSharp.Drawing/Processing/Brushes.cs
Normal file
841
ImageSharp.Drawing/Processing/Brushes.cs
Normal file
@ -0,0 +1,841 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// A collection of methods for creating generic brushes.
|
||||
/// </summary>
|
||||
public static partial class Brushes
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a solid color.
|
||||
/// </summary>
|
||||
/// <param name="color">The brush color.</param>
|
||||
/// <returns>A new <see cref="SolidBrush"/>.</returns>
|
||||
public static SolidBrush Solid(Color color) => new(color);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal line hatching using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Horizontal(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, HorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal line hatching using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Horizontal(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, HorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal line hatching for the minimum hatch style using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Min(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, HorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal line hatching for the minimum hatch style using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Min(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, HorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints vertical line hatching using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Vertical(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, VerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints vertical line hatching using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Vertical(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, VerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints diagonal line hatching from upper left to lower right using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush ForwardDiagonal(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, ForwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints diagonal line hatching from upper left to lower right using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush ForwardDiagonal(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, ForwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints diagonal line hatching from upper right to lower left using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush BackwardDiagonal(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, BackwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints diagonal line hatching from upper right to lower left using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush BackwardDiagonal(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, BackwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical line hatching using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Cross(Color foreColor) => new(foreColor, Color.Transparent, CrossPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical line hatching using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Cross(Color foreColor, Color backColor) => new(foreColor, backColor, CrossPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting forward and backward diagonal line hatching using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DiagonalCross(Color foreColor) => new(foreColor, Color.Transparent, DiagonalCrossPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting forward and backward diagonal line hatching using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DiagonalCross(Color foreColor, Color backColor) => new(foreColor, backColor, DiagonalCrossPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 5-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 5:95.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent05(Color foreColor) => new(foreColor, Color.Transparent, Percent05Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 5-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 5:95.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent05(Color foreColor, Color backColor) => new(foreColor, backColor, Percent05Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 10-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 10:90.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent10(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, Percent10Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 10-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 10:90.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent10(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, Percent10Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 20-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 20:80.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent20(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, Percent20Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 20-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 20:80.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent20(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, Percent20Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 25-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 25:75.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent25(Color foreColor) => new(foreColor, Color.Transparent, Percent25Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 25-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 25:75.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent25(Color foreColor, Color backColor) => new(foreColor, backColor, Percent25Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 30-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 30:70.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent30(Color foreColor) => new(foreColor, Color.Transparent, Percent30Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 30-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 30:70.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent30(Color foreColor, Color backColor) => new(foreColor, backColor, Percent30Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 40-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 40:60.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent40(Color foreColor) => new(foreColor, Color.Transparent, Percent40Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 40-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 40:60.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent40(Color foreColor, Color backColor) => new(foreColor, backColor, Percent40Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 50-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 50:50.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent50(Color foreColor) => new(foreColor, Color.Transparent, Percent50Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 50-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 50:50.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent50(Color foreColor, Color backColor) => new(foreColor, backColor, Percent50Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 60-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 60:40.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent60(Color foreColor) => new(foreColor, Color.Transparent, Percent60Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 60-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 60:40.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent60(Color foreColor, Color backColor) => new(foreColor, backColor, Percent60Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 70-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 70:30.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent70(Color foreColor) => new(foreColor, Color.Transparent, Percent70Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 70-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 70:30.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent70(Color foreColor, Color backColor) => new(foreColor, backColor, Percent70Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 75-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 75:25.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent75(Color foreColor) => new(foreColor, Color.Transparent, Percent75Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 75-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 75:25.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent75(Color foreColor, Color backColor) => new(foreColor, backColor, Percent75Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints an 80-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 80:20.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent80(Color foreColor) => new(foreColor, Color.Transparent, Percent80Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints an 80-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 80:20.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent80(Color foreColor, Color backColor) => new(foreColor, backColor, Percent80Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 90-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 90:10.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent90(Color foreColor) => new(foreColor, Color.Transparent, Percent90Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 90-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 90:10.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent90(Color foreColor, Color backColor) => new(foreColor, backColor, Percent90Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints downward diagonal lines spaced more closely than ForwardDiagonal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, LightDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints downward diagonal lines spaced more closely than ForwardDiagonal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, LightDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints upward diagonal lines spaced more closely than BackwardDiagonal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, LightUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints upward diagonal lines spaced more closely than BackwardDiagonal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, LightUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker downward diagonal lines spaced more closely than ForwardDiagonal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DarkDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker downward diagonal lines spaced more closely than ForwardDiagonal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DarkDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker upward diagonal lines spaced more closely than BackwardDiagonal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DarkUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker upward diagonal lines spaced more closely than BackwardDiagonal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DarkUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints wide downward diagonal lines with ForwardDiagonal spacing using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush WideDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, WideDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints wide downward diagonal lines with ForwardDiagonal spacing using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush WideDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, WideDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints wide upward diagonal lines with BackwardDiagonal spacing using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush WideUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, WideUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints wide upward diagonal lines with BackwardDiagonal spacing using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush WideUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, WideUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints vertical lines spaced more closely than Vertical using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightVertical(Color foreColor) => new(foreColor, Color.Transparent, LightVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints vertical lines spaced more closely than Vertical using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightVertical(Color foreColor, Color backColor) => new(foreColor, backColor, LightVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines spaced more closely than Horizontal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightHorizontal(Color foreColor) => new(foreColor, Color.Transparent, LightHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines spaced more closely than Horizontal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, LightHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints narrow vertical lines spaced more closely than LightVertical using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush NarrowVertical(Color foreColor) => new(foreColor, Color.Transparent, NarrowVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints narrow vertical lines spaced more closely than LightVertical using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush NarrowVertical(Color foreColor, Color backColor) => new(foreColor, backColor, NarrowVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints narrow horizontal lines spaced more closely than LightHorizontal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush NarrowHorizontal(Color foreColor) => new(foreColor, Color.Transparent, NarrowHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints narrow horizontal lines spaced more closely than LightHorizontal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush NarrowHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, NarrowHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker vertical lines spaced more closely than Vertical using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkVertical(Color foreColor) => new(foreColor, Color.Transparent, DarkVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker vertical lines spaced more closely than Vertical using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkVertical(Color foreColor, Color backColor) => new(foreColor, backColor, DarkVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker horizontal lines spaced more closely than Horizontal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkHorizontal(Color foreColor) => new(foreColor, Color.Transparent, DarkHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker horizontal lines spaced more closely than Horizontal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, DarkHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed diagonal lines from upper left to lower right using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DashedDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed diagonal lines from upper left to lower right using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DashedDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed diagonal lines from upper right to lower left using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DashedUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed diagonal lines from upper right to lower left using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DashedUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed horizontal lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedHorizontal(Color foreColor) => new(foreColor, Color.Transparent, DashedHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed horizontal lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, DashedHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed vertical lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedVertical(Color foreColor) => new(foreColor, Color.Transparent, DashedVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed vertical lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedVertical(Color foreColor, Color backColor) => new(foreColor, backColor, DashedVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a small confetti-style hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallConfetti(Color foreColor) => new(foreColor, Color.Transparent, SmallConfettiPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a small confetti-style hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallConfetti(Color foreColor, Color backColor) => new(foreColor, backColor, SmallConfettiPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a confetti-style hatch with larger pieces than SmallConfetti using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LargeConfetti(Color foreColor) => new(foreColor, Color.Transparent, LargeConfettiPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a confetti-style hatch with larger pieces than SmallConfetti using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LargeConfetti(Color foreColor, Color backColor) => new(foreColor, backColor, LargeConfettiPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines formed from zigzags using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush ZigZag(Color foreColor) => new(foreColor, Color.Transparent, ZigZagPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines formed from zigzags using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush ZigZag(Color foreColor, Color backColor) => new(foreColor, backColor, ZigZagPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines formed from wave shapes using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Wave(Color foreColor) => new(foreColor, Color.Transparent, WavePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines formed from wave shapes using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Wave(Color foreColor, Color backColor) => new(foreColor, backColor, WavePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints staggered brick shapes running diagonally upward using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DiagonalBrick(Color foreColor) => new(foreColor, Color.Transparent, DiagonalBrickPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints staggered brick shapes running diagonally upward using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DiagonalBrick(Color foreColor, Color backColor) => new(foreColor, backColor, DiagonalBrickPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints staggered brick shapes arranged horizontally using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush HorizontalBrick(Color foreColor) => new(foreColor, Color.Transparent, HorizontalBrickPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints staggered brick shapes arranged horizontally using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush HorizontalBrick(Color foreColor, Color backColor) => new(foreColor, backColor, HorizontalBrickPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a woven-material hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Weave(Color foreColor) => new(foreColor, Color.Transparent, WeavePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a woven-material hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Weave(Color foreColor, Color backColor) => new(foreColor, backColor, WeavePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a plaid-material hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Plaid(Color foreColor) => new(foreColor, Color.Transparent, PlaidPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a plaid-material hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Plaid(Color foreColor, Color backColor) => new(foreColor, backColor, PlaidPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a divot-style hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Divot(Color foreColor) => new(foreColor, Color.Transparent, DivotPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a divot-style hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Divot(Color foreColor, Color backColor) => new(foreColor, backColor, DivotPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical dotted lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DottedGrid(Color foreColor) => new(foreColor, Color.Transparent, DottedGridPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical dotted lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DottedGrid(Color foreColor, Color backColor) => new(foreColor, backColor, DottedGridPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting forward and backward diagonal dotted lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DottedDiamond(Color foreColor) => new(foreColor, Color.Transparent, DottedDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting forward and backward diagonal dotted lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DottedDiamond(Color foreColor, Color backColor) => new(foreColor, backColor, DottedDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints layered shingle shapes running diagonally downward using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Shingle(Color foreColor) => new(foreColor, Color.Transparent, ShinglePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints layered shingle shapes running diagonally downward using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Shingle(Color foreColor, Color backColor) => new(foreColor, backColor, ShinglePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a trellis-style hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Trellis(Color foreColor) => new(foreColor, Color.Transparent, TrellisPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a trellis-style hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Trellis(Color foreColor, Color backColor) => new(foreColor, backColor, TrellisPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints adjacent sphere-like shapes using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Sphere(Color foreColor) => new(foreColor, Color.Transparent, SpherePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints adjacent sphere-like shapes using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Sphere(Color foreColor, Color backColor) => new(foreColor, backColor, SpherePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical lines spaced more closely than Cross using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallGrid(Color foreColor) => new(foreColor, Color.Transparent, SmallGridPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical lines spaced more closely than Cross using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallGrid(Color foreColor, Color backColor) => new(foreColor, backColor, SmallGridPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a small checkerboard hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallCheckerBoard(Color foreColor) => new(foreColor, Color.Transparent, SmallCheckerBoardPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a small checkerboard hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallCheckerBoard(Color foreColor, Color backColor) => new(foreColor, backColor, SmallCheckerBoardPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a checkerboard hatch with larger squares than SmallCheckerBoard using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LargeCheckerBoard(Color foreColor) => new(foreColor, Color.Transparent, LargeCheckerBoardPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a checkerboard hatch with larger squares than SmallCheckerBoard using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LargeCheckerBoard(Color foreColor, Color backColor) => new(foreColor, backColor, LargeCheckerBoardPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints outlined diamond shapes formed by crossing diagonal lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush OutlinedDiamond(Color foreColor) => new(foreColor, Color.Transparent, OutlinedDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints outlined diamond shapes formed by crossing diagonal lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush OutlinedDiamond(Color foreColor, Color backColor) => new(foreColor, backColor, OutlinedDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a filled diamond checkerboard hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SolidDiamond(Color foreColor) => new(foreColor, Color.Transparent, SolidDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a filled diamond checkerboard hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SolidDiamond(Color foreColor, Color backColor) => new(foreColor, backColor, SolidDiamondPattern);
|
||||
}
|
||||
}
|
||||
34
ImageSharp.Drawing/Processing/ColorStop.cs
Normal file
34
ImageSharp.Drawing/Processing/ColorStop.cs
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// A struct that defines a single color stop.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("ColorStop({Ratio} -> {Color}")]
|
||||
public readonly struct ColorStop
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColorStop" /> struct.
|
||||
/// </summary>
|
||||
/// <param name="ratio">Where should it be? 0 is at the start, 1 at the end of the Gradient.</param>
|
||||
/// <param name="color">What color should be used at that point?</param>
|
||||
public ColorStop(float ratio, in Color color)
|
||||
{
|
||||
this.Ratio = ratio;
|
||||
this.Color = color;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the point along the defined gradient axis.
|
||||
/// </summary>
|
||||
public float Ratio { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color to be used.
|
||||
/// </summary>
|
||||
public Color Color { get; }
|
||||
}
|
||||
}
|
||||
392
ImageSharp.Drawing/Processing/DRAWING_CANVAS.md
Normal file
392
ImageSharp.Drawing/Processing/DRAWING_CANVAS.md
Normal file
@ -0,0 +1,392 @@
|
||||
# DrawingCanvas
|
||||
|
||||
`DrawingCanvas` is the high-level drawing surface used by ImageSharp.Drawing. It lets the library expose one drawing model while supporting very different execution targets:
|
||||
|
||||
- CPU rasterization into memory
|
||||
- GPU execution through native surfaces
|
||||
- backends that prefer their own internal representation, such as vector export
|
||||
|
||||
That unification is the hard part. The public API wants to feel immediate and simple: fill a path, draw text, save state, restore state, draw into a region, maybe draw into a layer. The backends, however, do not all want the same kind of work. A CPU rasterizer wants rows, spans, and direct pixel access. A GPU backend wants compact command data, stable batching, and a single handoff point. A vector exporter would want semantic geometry rather than already-rasterized pixels.
|
||||
|
||||
The architecture around `DrawingCanvas` and its typed implementation exists to absorb that mismatch.
|
||||
|
||||
This document explains that architecture from the outside in. The goal is to help a newcomer understand what problem each piece solves before diving into methods and types.
|
||||
|
||||
## The Main Problem
|
||||
|
||||
If the canvas executed every public call immediately, each backend would have to implement the entire public drawing model directly:
|
||||
|
||||
- save and restore state
|
||||
- clip stacking
|
||||
- layers and isolated composition
|
||||
- text drawing
|
||||
- image drawing with transforms
|
||||
- region drawing
|
||||
- brush and pen handling
|
||||
- transform handling
|
||||
|
||||
That sounds straightforward until the differences between backends become obvious.
|
||||
|
||||
The CPU backend can cheaply mutate a memory buffer row by row. The GPU backend wants a larger batch of work so it can amortize setup, upload, and dispatch costs. A vector-style backend would ideally preserve geometry and draw intent for as long as possible. If each backend solved all of that from scratch, they would drift apart quickly and correctness bugs would multiply.
|
||||
|
||||
So the architecture chooses a different approach:
|
||||
|
||||
`DrawingCanvas` records drawing intent first, normalizes that intent when replaying or creating a retained scene, and only then hands the work to the backend.
|
||||
|
||||
That one decision explains most of the surrounding design.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
The canvas is a deferred renderer.
|
||||
|
||||
Drawing calls do not rasterize immediately. They create `CompositionCommand` records and queue them into `DrawingCanvasBatcher<TPixel>`. The expensive normalization work happens later, during replay, when the batcher prepares those commands and hands `DrawingCommandBatch` ranges to the backend.
|
||||
|
||||
That gives the architecture three important benefits.
|
||||
|
||||
First, the public API stays backend-agnostic. A fill is a fill, whether the target is CPU memory or a GPU surface.
|
||||
|
||||
Second, the expensive shared command work can happen once, in one shared place. Transform application, stroke expansion, clip application, and dash expansion are not reimplemented independently by every backend.
|
||||
|
||||
Third, the backend receives a much more stable handoff. Instead of reacting to a long stream of public API calls, it receives prepared command batches with consistent semantics.
|
||||
|
||||
## The Most Important Terms
|
||||
|
||||
Before looking at the flow, it helps to define the major terms in the sense used by this codebase.
|
||||
|
||||
### Canvas
|
||||
|
||||
`DrawingCanvas` is the public drawing facade. It owns the current drawing state, accepts commands, and decides when to flush.
|
||||
|
||||
It is not the rasterizer. It is the object that makes the public drawing model coherent.
|
||||
|
||||
Callers usually reach that model through `IImageProcessingContext.Paint(...)`.
|
||||
|
||||
When callers already have an `ImageFrame` or `ImageFrame<TPixel>`, the public `CreateCanvas(...)` frame extensions create a canvas directly over that frame. The caller owns the returned canvas and must dispose it to replay recorded work into the frame.
|
||||
|
||||
`DrawingCanvas<TPixel>` is the typed implementation that carries the target pixel format for brush normalization,
|
||||
readback, and backend execution. Factory methods return `DrawingCanvas` so CPU and GPU entry points expose the same
|
||||
canvas-facing API while still constructing the typed implementation internally.
|
||||
|
||||
### Batcher
|
||||
|
||||
`DrawingCanvasBatcher<TPixel>` is the deferred command queue. It stores pending `CompositionCommand` values, records the canvas replay timeline, prepares commands during replay, and creates `DrawingCommandBatch` values for command-range entries.
|
||||
|
||||
It is the bridge between the immediate-looking public API and the deferred backend handoff.
|
||||
|
||||
### Command
|
||||
|
||||
`CompositionCommand` is the recorded unit of drawing intent. In the common case it means "fill this path with this brush under this state". The command stream also carries explicit layer boundaries through `BeginLayer` and `EndLayer`.
|
||||
|
||||
The command remains relatively close to the original user request. It may hold the original path, pen, brush, transform, and clip paths.
|
||||
|
||||
### Preparation
|
||||
|
||||
Preparation is the normalization step that turns recorded intent into backend-ready commands.
|
||||
|
||||
`DrawingCanvasBatcher<TPixel>.PrepareCommands(...)` runs only when needed. It applies command transforms, expands strokes to fill paths, applies clip paths so clipped commands reach the backend as ordinary fills, and expands dashed strokes when a stroke pattern is present.
|
||||
|
||||
Preparation stops at `DrawingCommandBatch`. Backend-specific lowering happens after that, inside `IDrawingBackend.CreateScene(...)`.
|
||||
|
||||
### Command Batch
|
||||
|
||||
`DrawingCommandBatch` is the prepared command range handed to the backend. It contains the command stream for one contiguous range and scene-level facts such as whether that range contains layer boundaries.
|
||||
|
||||
It is the backend handoff boundary.
|
||||
|
||||
### Backend
|
||||
|
||||
`IDrawingBackend` is the execution engine behind the canvas. The important implementations are:
|
||||
|
||||
- `DefaultDrawingBackend` for CPU rendering
|
||||
- `WebGPUDrawingBackend` for GPU rendering through native surfaces
|
||||
|
||||
The backend creates retained scenes from command batches and renders retained scenes into typed target frames.
|
||||
|
||||
There are two backend-selection paths in the architecture:
|
||||
|
||||
- direct `DrawingCanvas<TPixel>` construction resolves the backend from `Configuration`
|
||||
- specialized infrastructure can construct a canvas with an explicit backend
|
||||
|
||||
The ordinary CPU entry point is `Paint(...)` on `IImageProcessingContext`, which routes into the typed
|
||||
implementation internally. Public `ImageFrame` canvas extensions provide the lower-level frame entry point for callers that want to own the canvas lifetime directly.
|
||||
|
||||
That explicit-backend path matters for the WebGPU helpers. `WebGPUWindow`, `WebGPUExternalSurface`, and `WebGPURenderTarget` create canvases that point directly at their owned `WebGPUDrawingBackend` instance instead of storing that backend on the caller's `Configuration`.
|
||||
|
||||
### Frame
|
||||
|
||||
`ICanvasFrame<TPixel>` is the target abstraction that the backend renders into.
|
||||
|
||||
This is one of the terms that can be ambiguous without context, so it is worth being explicit. In this architecture, a canvas frame is not "a UI frame" or "one animation frame". It means "the destination surface for one canvas instance".
|
||||
|
||||
The important properties of a frame are:
|
||||
|
||||
- `Bounds`
|
||||
- whether it exposes a CPU region through `TryGetCpuRegion(...)`
|
||||
- whether it exposes a native surface through `TryGetNativeSurface(...)`
|
||||
|
||||
That abstraction lets the same canvas target:
|
||||
|
||||
- pure CPU memory with `MemoryCanvasFrame<TPixel>`
|
||||
- a native or GPU surface with `NativeCanvasFrame<TPixel>`
|
||||
- a combined CPU plus native target
|
||||
- a clipped view over another frame with `CanvasRegionFrame<TPixel>`
|
||||
|
||||
The point is not to hide all differences. The point is to express the minimum target contract the backends need.
|
||||
|
||||
### Layer
|
||||
|
||||
A layer is isolated group rendering. In public API terms, it is created with `SaveLayer(...)` and later closed by `Restore()` or `RestoreTo(...)`.
|
||||
|
||||
In this architecture, a layer is recorded inline in the command stream as:
|
||||
|
||||
- `BeginLayer`
|
||||
- commands inside the layer
|
||||
- `EndLayer`
|
||||
|
||||
The backend is responsible for lowering those layer boundaries into the execution model it needs.
|
||||
|
||||
Layer semantics stay in the shared command model so every backend receives the same layer structure at the handoff boundary.
|
||||
|
||||
## The Big Picture Flow
|
||||
|
||||
The easiest way to understand the system is to follow one normal draw call all the way through.
|
||||
|
||||
### Step 1: The canvas records intent
|
||||
|
||||
A public method such as `Fill(...)`, `Draw(...)`, or `DrawText(...)` resolves the active state and creates one or more `CompositionCommand` values.
|
||||
|
||||
At this point the canvas is mostly recording:
|
||||
|
||||
- geometry references
|
||||
- brushes or pens
|
||||
- active transform
|
||||
- clip paths
|
||||
- graphics options
|
||||
- target bounds relevant to this command
|
||||
|
||||
The canvas does not try to fully rasterize anything here.
|
||||
|
||||
### Step 2: The batcher owns the pending work
|
||||
|
||||
Commands go into `DrawingCanvasBatcher<TPixel>`.
|
||||
|
||||
The batcher exists so the canvas does not need to talk to the backend for every single API call. It accumulates work until a timeline boundary is reached.
|
||||
|
||||
The replay boundary usually comes from:
|
||||
|
||||
- explicit `Flush()`, which seals the current command range
|
||||
- `Apply(...)`, which needs read-modify-write behavior
|
||||
- `RenderScene(...)`, which inserts an existing retained scene into the timeline
|
||||
- disposal of the owning canvas
|
||||
|
||||
### Step 3: The batcher prepares commands
|
||||
|
||||
When the root canvas is disposed, or when the caller creates a retained scene, the batcher seals any pending commands and prepares the command buffer. This is where the architecture does the heavy shared work that would otherwise be duplicated across backends.
|
||||
|
||||
For a typical path-based command, canvas preparation does the following in concept:
|
||||
|
||||
1. transform the source path into its final geometry space
|
||||
2. if a pen is present, expand the stroke to fill geometry
|
||||
3. apply clip paths
|
||||
4. transform the brush into the same command space
|
||||
5. leave backend-specific retained geometry construction to `CreateScene(...)`
|
||||
|
||||
This is the architectural center of gravity. It is the shared normalization stage that makes the backends simpler.
|
||||
|
||||
### Step 4: The backend creates and renders scenes
|
||||
|
||||
After preparation, disposal replay walks the canvas timeline in order. Command-range entries become short-lived retained scenes through `backend.CreateScene(...)`, and those scenes are then rendered through `backend.RenderScene(...)`.
|
||||
|
||||
From that point the CPU and GPU paths diverge.
|
||||
|
||||
The CPU backend lowers each command batch into a row-oriented retained representation through `FlushScene` during `CreateScene(...)` and then composites into memory during `RenderScene(...)`.
|
||||
|
||||
The WebGPU backend encodes each command batch into its retained GPU representation during `CreateScene(...)`, then uploads render-scoped resources and dispatches GPU work during `RenderScene(...)`.
|
||||
|
||||
The architecture is successful if both backends can differ dramatically here without needing the public canvas model itself to fork.
|
||||
|
||||
## Why State Is Snapshotted
|
||||
|
||||
Drawing APIs look stateful because they are stateful. The active transform, clips, graphics options, and layer information all affect future commands.
|
||||
|
||||
`DrawingCanvasState` exists so that state changes are cheap to reason about and cheap to attach to commands.
|
||||
|
||||
The state snapshot contains the active options and target information for subsequent commands, including:
|
||||
|
||||
- `Options`
|
||||
- `ClipPaths`
|
||||
- `IsLayer`
|
||||
- layer-related graphics options and bounds
|
||||
- current target bounds
|
||||
|
||||
The canvas treats this state as immutable snapshots on a stack. `Save()` pushes a copy. `Restore()` pops one. Drawing calls always read the current top-of-stack state.
|
||||
|
||||
That makes save and restore semantics predictable and backend-independent.
|
||||
|
||||
## How Layers Work In This Architecture
|
||||
|
||||
Layer terminology often causes confusion because different systems use it differently. In this codebase, the most useful mental model is:
|
||||
|
||||
"A layer is a nested composition scope recorded inline in the command stream."
|
||||
|
||||
When `SaveLayer(...)` is called, the canvas:
|
||||
|
||||
1. clamps the requested layer bounds to the canvas
|
||||
2. converts them into absolute target bounds
|
||||
3. records `BeginLayer`
|
||||
4. pushes a state snapshot that marks the new layer scope
|
||||
|
||||
The layer bounds are expressed in the active local coordinate system, so the canvas
|
||||
transform in effect at `SaveLayer(...)` time is applied when resolving the layer's
|
||||
absolute target bounds. The resolved bounds limit isolation, allocation, and final
|
||||
composition. They do not shift the canvas coordinate system; draw commands inside a
|
||||
bounded layer still use the same local coordinates as the parent canvas.
|
||||
|
||||
When the layer is later closed through `Restore()` or `RestoreTo(...)`, the canvas records `EndLayer`.
|
||||
|
||||
The actual isolation is implemented later by the backend.
|
||||
|
||||
On the CPU backend, layer boundaries become temporary backing buffers during scene execution.
|
||||
|
||||
On the WebGPU backend, layer boundaries become explicit staged-scene operations inside the GPU-oriented pipeline.
|
||||
|
||||
The key architectural point is that the public canvas records one shared layer model and lets the backend lower it.
|
||||
|
||||
## Why Frames Exist
|
||||
|
||||
The frame abstraction solves another unification problem.
|
||||
|
||||
The canvas should be able to target a plain in-memory image, but that should not force the GPU backend to pretend everything is CPU memory. Likewise, GPU-native targets should not force the CPU path to know about native surfaces directly.
|
||||
|
||||
`ICanvasFrame<TPixel>` is the contract that keeps those concerns separated.
|
||||
|
||||
In this architecture, a frame means "the destination surface and its capabilities". That is why the interface exposes both:
|
||||
|
||||
- geometric bounds
|
||||
- optional CPU access
|
||||
- optional native-surface access
|
||||
|
||||
This lets the same canvas code target different kinds of surfaces without rewriting the command model.
|
||||
|
||||
`CanvasRegionFrame<TPixel>` extends that idea one step further by saying "treat this clipped rectangle inside another frame as the target". That is how region canvases can share the same backend and batcher model while still drawing into a sub-rectangle.
|
||||
|
||||
## What `CreateRegion(...)` Really Means
|
||||
|
||||
`CreateRegion(...)` does not create a new independent rendering universe. It creates a child canvas that views a clipped sub-region of the parent target.
|
||||
|
||||
The child:
|
||||
|
||||
- wraps the parent target in `CanvasRegionFrame<TPixel>`
|
||||
- keeps using the same backend
|
||||
- keeps using the same shared batcher
|
||||
- keeps participating in the same deferred replay model
|
||||
|
||||
The child canvas has local coordinates starting at `(0, 0)`, but its frame bounds resolve to the correct absolute position inside the parent target.
|
||||
|
||||
That distinction matters. It means the region API is a coordinate-system convenience, not a request to fork rendering into a totally separate backend pipeline.
|
||||
|
||||
## Why `DrawImage(...)` Is Special
|
||||
|
||||
Most draw calls record intent and defer the heavy work.
|
||||
|
||||
`DrawImage(...)` is the notable exception.
|
||||
|
||||
Images behave differently from paths because the canvas cannot simply attach a transform and let the backend "figure it out later" in the same way. The code performs eager image work before the final command is queued.
|
||||
|
||||
The rough flow is:
|
||||
|
||||
1. crop and scale the source image if needed
|
||||
2. if a canvas transform is active, bake that transform into the image pixels
|
||||
3. align the transformed bitmap to integer canvas bounds
|
||||
4. create an `ImageBrush`
|
||||
5. queue the final fill command using that brush
|
||||
|
||||
This design avoids applying the canvas transform twice and keeps the later command model consistent with brush-based filling.
|
||||
|
||||
That is why `DrawImage(...)` should be understood as "prepare an image-backed brush, then queue a normal fill", not as a completely separate rasterization pipeline.
|
||||
|
||||
## What The CPU Backend Receives
|
||||
|
||||
Once a command batch reaches `DefaultDrawingBackend.CreateScene(...)`, the public drawing model is already normalized.
|
||||
|
||||
The CPU backend does not need to understand every public API call individually. It works with:
|
||||
|
||||
- prepared commands
|
||||
- layer boundaries
|
||||
- target bounds during `CreateScene(...)`
|
||||
- the destination frame during `RenderScene<TPixel>(...)`
|
||||
|
||||
It lowers each command batch into a retained row-oriented structure through `FlushScene`. Later, `RenderScene<TPixel>(...)` acquires the CPU destination frame, allocates temporary backing buffers for layers when needed, and composites the final result into the target frame.
|
||||
|
||||
That is the payoff of the architecture: the CPU backend is solving a rendering problem, not a public-API interpretation problem.
|
||||
|
||||
## What The WebGPU Backend Receives
|
||||
|
||||
The WebGPU backend receives the same command batch shape, but it splits retained scene creation from render-scoped GPU work.
|
||||
|
||||
`CreateScene(...)` handles:
|
||||
|
||||
- encoding prepared command data
|
||||
|
||||
`RenderScene<TPixel>(...)` handles:
|
||||
|
||||
- creating render-scoped native resources
|
||||
- planning dispatches
|
||||
- executing the GPU pipeline
|
||||
|
||||
It benefits from the same canvas-level decisions:
|
||||
|
||||
- commands are already normalized
|
||||
- layers already exist as explicit boundaries
|
||||
- the frame already describes whether a native surface is available
|
||||
|
||||
The WebGPU public helpers reach this point in a target-first way:
|
||||
|
||||
- `WebGPUWindow` acquires a presentable native target per frame
|
||||
- `WebGPURenderTarget` owns an offscreen native target for GPU drawing and readback
|
||||
- `WebGPUExternalSurface` attaches WebGPU drawing to a caller-owned native host
|
||||
|
||||
Those helpers all create typed canvas instances with an explicit `WebGPUDrawingBackend`, so GPU execution stays attached to the WebGPU object that owns the native target and backend lifetime while callers work through `DrawingCanvas`.
|
||||
|
||||
The backend is free to choose a very different execution model because the canvas has already solved the shared semantics problem.
|
||||
|
||||
## The Practical Mental Model
|
||||
|
||||
If you are new to this code, the most useful mental model is:
|
||||
|
||||
`DrawingCanvas` is the stateful front end that records drawing intent, `DrawingCanvas<TPixel>` is the typed implementation, `DrawingCanvasBatcher<TPixel>` is the deferred handoff boundary, and the backend creates and renders retained scenes from prepared command batches.
|
||||
|
||||
Everything else serves that flow.
|
||||
|
||||
State snapshots exist so save and restore are precise.
|
||||
|
||||
Commands exist so public API calls can be deferred.
|
||||
|
||||
Preparation exists so backend-agnostic normalization happens once.
|
||||
|
||||
Frames exist so the same canvas can target memory, native surfaces, or sub-regions.
|
||||
|
||||
Layers exist as inline composition scopes in the command stream.
|
||||
|
||||
Once those ideas are clear, the code stops looking like a random collection of types and starts looking like one system with a clear division of responsibility.
|
||||
|
||||
## Reading Guide
|
||||
|
||||
If you want to move from the architecture into the code, this is the best order.
|
||||
|
||||
1. `DrawingCanvas.cs`
|
||||
2. `DrawingCanvas{TPixel}.cs`
|
||||
3. `DrawingCanvasFactoryExtensions.cs` and `DrawingCanvas.Shapes.cs`
|
||||
4. `DrawingCanvasBatcher{TPixel}.cs`
|
||||
5. `CompositionCommand.cs`
|
||||
6. `DefaultDrawingBackend.cs`
|
||||
7. `FlushScene.cs`
|
||||
8. `WebGPUEnvironment.cs`
|
||||
9. `WebGPUWindow.cs`, `WebGPUExternalSurface.cs`, and `WebGPURenderTarget.cs`
|
||||
10. `WebGPUDrawingBackend` and its scene/dispatch types
|
||||
|
||||
That path follows the real runtime flow:
|
||||
|
||||
public API -> recorded command -> prepared command batch -> backend scene creation -> backend scene rendering
|
||||
|
||||
Following the code in that order is much easier than starting from the backend internals first.
|
||||
225
ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs
Normal file
225
ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs
Normal file
@ -0,0 +1,225 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <content>
|
||||
/// Convenience shape helpers that forward to the core <see cref="DrawingCanvas"/> primitives.
|
||||
/// </content>
|
||||
public abstract partial class DrawingCanvas
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the current drawing state and begins an isolated compositing layer over the whole canvas.
|
||||
/// </summary>
|
||||
/// <returns>The save count after the layer state has been pushed.</returns>
|
||||
public int SaveLayer()
|
||||
=> this.SaveLayer(new GraphicsOptions(), this.Bounds);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current drawing state and begins an isolated compositing layer over the whole canvas.
|
||||
/// </summary>
|
||||
/// <param name="layerOptions">Graphics options controlling how the layer is composited on restore.</param>
|
||||
/// <returns>The save count after the layer state has been pushed.</returns>
|
||||
public int SaveLayer(GraphicsOptions layerOptions)
|
||||
=> this.SaveLayer(layerOptions, this.Bounds);
|
||||
|
||||
/// <summary>
|
||||
/// Fills the whole canvas using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels.</param>
|
||||
public void Fill(Brush brush)
|
||||
{
|
||||
Rectangle bounds = this.Bounds;
|
||||
|
||||
this.Fill(brush, new RectanglePolygon(bounds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills a local region using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels.</param>
|
||||
/// <param name="region">Region to fill in local coordinates.</param>
|
||||
public void Fill(Brush brush, Rectangle region)
|
||||
=> this.Fill(brush, new RectanglePolygon(region));
|
||||
|
||||
/// <summary>
|
||||
/// Clears the whole canvas using the given brush and clear-style composition options.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels during clear.</param>
|
||||
public void Clear(Brush brush)
|
||||
{
|
||||
Rectangle bounds = this.Bounds;
|
||||
|
||||
this.Clear(brush, new RectanglePolygon(bounds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears a local region using the given brush and clear-style composition options.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels during clear.</param>
|
||||
/// <param name="region">Region to clear in local coordinates.</param>
|
||||
public void Clear(Brush brush, Rectangle region)
|
||||
=> this.Clear(brush, new RectanglePolygon(region));
|
||||
|
||||
/// <summary>
|
||||
/// Fills all paths in a collection using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="paths">Path collection to fill.</param>
|
||||
public void Fill(Brush brush, IPathCollection paths)
|
||||
{
|
||||
Guard.NotNull(paths, nameof(paths));
|
||||
|
||||
foreach (IPath path in paths)
|
||||
{
|
||||
this.Fill(brush, path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills a path built by the provided builder using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="pathBuilder">The path builder describing the fill region.</param>
|
||||
public void Fill(Brush brush, PathBuilder pathBuilder)
|
||||
{
|
||||
Guard.NotNull(pathBuilder, nameof(pathBuilder));
|
||||
|
||||
this.Fill(brush, pathBuilder.Build());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills an ellipse using the provided brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="center">Ellipse center point in local coordinates.</param>
|
||||
/// <param name="size">Ellipse width and height in local coordinates.</param>
|
||||
public void FillEllipse(Brush brush, PointF center, SizeF size)
|
||||
=> this.Fill(brush, new EllipsePolygon(center, size));
|
||||
|
||||
/// <summary>
|
||||
/// Fills the closed arc shape produced by joining the arc endpoints with a straight line.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="center">Arc center point in local coordinates.</param>
|
||||
/// <param name="radius">Arc radii in local coordinates.</param>
|
||||
/// <param name="rotation">Ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">Arc start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">Arc sweep angle in degrees.</param>
|
||||
public void FillArc(Brush brush, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.Fill(brush, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle)));
|
||||
|
||||
/// <summary>
|
||||
/// Fills a pie sector using the provided brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="center">The center point of the pie sector in local coordinates.</param>
|
||||
/// <param name="radius">The x and y radii of the pie sector in local coordinates.</param>
|
||||
/// <param name="rotation">Ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">The start angle of the pie sector in degrees.</param>
|
||||
/// <param name="sweepAngle">The sweep angle of the pie sector in degrees.</param>
|
||||
public void FillPie(Brush brush, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.Fill(brush, new PiePolygon(center, radius, rotation, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Fills a pie sector using the provided brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="center">The center point of the pie sector in local coordinates.</param>
|
||||
/// <param name="radius">The x and y radii of the pie sector in local coordinates.</param>
|
||||
/// <param name="startAngle">The start angle of the pie sector in degrees.</param>
|
||||
/// <param name="sweepAngle">The sweep angle of the pie sector in degrees.</param>
|
||||
public void FillPie(Brush brush, PointF center, SizeF radius, float startAngle, float sweepAngle)
|
||||
=> this.Fill(brush, new PiePolygon(center, radius, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Draws an arc outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the arc outline.</param>
|
||||
/// <param name="center">Arc center point in local coordinates.</param>
|
||||
/// <param name="radius">Arc radii in local coordinates.</param>
|
||||
/// <param name="rotation">Ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">Arc start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">Arc sweep angle in degrees.</param>
|
||||
public void DrawArc(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.Draw(pen, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle)));
|
||||
|
||||
/// <summary>
|
||||
/// Draws a cubic bezier outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the bezier outline.</param>
|
||||
/// <param name="points">Bezier control points.</param>
|
||||
public void DrawBezier(Pen pen, params PointF[] points)
|
||||
{
|
||||
Guard.NotNull(points, nameof(points));
|
||||
|
||||
this.Draw(pen, new Path(new CubicBezierLineSegment(points)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws an ellipse outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the ellipse outline.</param>
|
||||
/// <param name="center">Ellipse center point in local coordinates.</param>
|
||||
/// <param name="size">Ellipse width and height in local coordinates.</param>
|
||||
public void DrawEllipse(Pen pen, PointF center, SizeF size)
|
||||
=> this.Draw(pen, new EllipsePolygon(center, size));
|
||||
|
||||
/// <summary>
|
||||
/// Draws a pie sector outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the pie outline.</param>
|
||||
/// <param name="center">The center point of the pie sector in local coordinates.</param>
|
||||
/// <param name="radius">The x and y radii of the pie sector in local coordinates.</param>
|
||||
/// <param name="rotation">Ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">The start angle of the pie sector in degrees.</param>
|
||||
/// <param name="sweepAngle">The sweep angle of the pie sector in degrees.</param>
|
||||
public void DrawPie(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.Draw(pen, new PiePolygon(center, radius, rotation, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Draws a pie sector outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the pie outline.</param>
|
||||
/// <param name="center">The center point of the pie sector in local coordinates.</param>
|
||||
/// <param name="radius">The x and y radii of the pie sector in local coordinates.</param>
|
||||
/// <param name="startAngle">The start angle of the pie sector in degrees.</param>
|
||||
/// <param name="sweepAngle">The sweep angle of the pie sector in degrees.</param>
|
||||
public void DrawPie(Pen pen, PointF center, SizeF radius, float startAngle, float sweepAngle)
|
||||
=> this.Draw(pen, new PiePolygon(center, radius, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Draws a rectangular outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the rectangle outline.</param>
|
||||
/// <param name="region">Rectangle region to stroke.</param>
|
||||
public void Draw(Pen pen, Rectangle region)
|
||||
=> this.Draw(pen, new RectanglePolygon(region));
|
||||
|
||||
/// <summary>
|
||||
/// Draws all paths in a collection using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate outlines.</param>
|
||||
/// <param name="paths">Path collection to stroke.</param>
|
||||
public void Draw(Pen pen, IPathCollection paths)
|
||||
{
|
||||
Guard.NotNull(paths, nameof(paths));
|
||||
|
||||
foreach (IPath path in paths)
|
||||
{
|
||||
this.Draw(pen, path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a path outline built by the provided builder using the given pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the outline fill path.</param>
|
||||
/// <param name="pathBuilder">The path builder describing the path to stroke.</param>
|
||||
public void Draw(Pen pen, PathBuilder pathBuilder)
|
||||
{
|
||||
Guard.NotNull(pathBuilder, nameof(pathBuilder));
|
||||
|
||||
this.Draw(pen, pathBuilder.Build());
|
||||
}
|
||||
}
|
||||
}
|
||||
294
ImageSharp.Drawing/Processing/DrawingCanvas.cs
Normal file
294
ImageSharp.Drawing/Processing/DrawingCanvas.cs
Normal file
@ -0,0 +1,294 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.ImageSharp.Drawing.Processing.Backends;
|
||||
using SixLabors.ImageSharp.Drawing.Text;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Represents a drawing canvas over a frame target.
|
||||
/// </summary>
|
||||
public abstract partial class DrawingCanvas : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the local bounds of this canvas.
|
||||
/// </summary>
|
||||
public abstract Rectangle Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of saved states currently on the canvas stack.
|
||||
/// </summary>
|
||||
public abstract int SaveCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current drawing state on the state stack.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This operation stores the current canvas state by reference.
|
||||
/// If the same <see cref="DrawingOptions"/> instance is mutated after
|
||||
/// <see cref="Save()"/>, those mutations are visible when restoring.
|
||||
/// </remarks>
|
||||
/// <returns>The save count after the state has been pushed.</returns>
|
||||
public abstract int Save();
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current drawing state and replaces the active state with the provided options and clip paths.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The provided <paramref name="options"/> instance is stored by reference.
|
||||
/// Mutating it after this call mutates the active/restored state behavior.
|
||||
/// </remarks>
|
||||
/// <param name="options">Drawing options for the new active state.</param>
|
||||
/// <param name="clipPaths">Clip paths for the new active state.</param>
|
||||
/// <returns>The save count after the previous state has been pushed.</returns>
|
||||
public abstract int Save(DrawingOptions options, params IPath[] clipPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current drawing state and begins an isolated compositing layer
|
||||
/// bounded to a subregion. Subsequent draw commands are recorded into that isolated
|
||||
/// logical layer. When <see cref="Restore"/> closes the layer, it is recorded into the
|
||||
/// canvas timeline and later composed during <see cref="IDisposable.Dispose"/> using the specified
|
||||
/// <paramref name="layerOptions"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The layer bounds are expressed in the current local coordinate system and are
|
||||
/// transformed with the active drawing transform when the layer is created. They
|
||||
/// limit allocation and compositing only; they do not change the canvas coordinate
|
||||
/// system used by commands recorded inside the layer.
|
||||
/// </remarks>
|
||||
/// <param name="layerOptions">
|
||||
/// Graphics options controlling how the closed layer is composited against the parent canvas
|
||||
/// when the canvas timeline is rendered during <see cref="IDisposable.Dispose"/>.
|
||||
/// </param>
|
||||
/// <param name="bounds">
|
||||
/// The local bounds of the layer. Only this region is allocated and composited.
|
||||
/// </param>
|
||||
/// <returns>The save count after the layer state has been pushed.</returns>
|
||||
public abstract int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds);
|
||||
|
||||
/// <summary>
|
||||
/// Restores the most recently saved state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the most recently saved state was created by a <c>SaveLayer</c> overload,
|
||||
/// the layer is closed in the recorded timeline. Actual composition happens during
|
||||
/// <see cref="IDisposable.Dispose"/>.
|
||||
/// </remarks>
|
||||
public abstract void Restore();
|
||||
|
||||
/// <summary>
|
||||
/// Restores to a specific save count.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// State frames above <paramref name="saveCount"/> are discarded,
|
||||
/// and the last discarded frame becomes the current state.
|
||||
/// If any discarded state was created by a <c>SaveLayer</c> overload,
|
||||
/// those layers are closed in the recorded timeline and composed during
|
||||
/// <see cref="IDisposable.Dispose"/>.
|
||||
/// </remarks>
|
||||
/// <param name="saveCount">The save count to restore to.</param>
|
||||
public abstract void RestoreTo(int saveCount);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a child canvas over a subregion in local coordinates.
|
||||
/// </summary>
|
||||
/// <param name="region">The child region in local coordinates.</param>
|
||||
/// <returns>A child canvas with local origin at (0,0).</returns>
|
||||
public abstract DrawingCanvas CreateRegion(Rectangle region);
|
||||
|
||||
/// <summary>
|
||||
/// Clears a path region using the given brush and clear-style composition options.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels during clear.</param>
|
||||
/// <param name="path">The path region to clear.</param>
|
||||
public abstract void Clear(Brush brush, IPath path);
|
||||
|
||||
/// <summary>
|
||||
/// Fills a path in local coordinates using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="path">The path to fill.</param>
|
||||
public abstract void Fill(Brush brush, IPath path);
|
||||
|
||||
/// <summary>
|
||||
/// Applies an image-processing operation to a local region.
|
||||
/// </summary>
|
||||
/// <param name="region">The local region to process.</param>
|
||||
/// <param name="operation">The image-processing operation to apply to the region.</param>
|
||||
public abstract void Apply(Rectangle region, Action<IImageProcessingContext> operation);
|
||||
|
||||
/// <summary>
|
||||
/// Applies an image-processing operation to a region described by a path builder.
|
||||
/// </summary>
|
||||
/// <param name="pathBuilder">The path builder describing the region to process.</param>
|
||||
/// <param name="operation">The image-processing operation to apply to the region.</param>
|
||||
public abstract void Apply(PathBuilder pathBuilder, Action<IImageProcessingContext> operation);
|
||||
|
||||
/// <summary>
|
||||
/// Applies an image-processing operation to a path region.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The operation affects only pixels covered by the supplied path.
|
||||
/// </remarks>
|
||||
/// <param name="path">The path region to process.</param>
|
||||
/// <param name="operation">The image-processing operation to apply to the region.</param>
|
||||
public abstract void Apply(IPath path, Action<IImageProcessingContext> operation);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a polyline outline using the provided pen and drawing options.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the line outline.</param>
|
||||
/// <param name="points">Polyline points.</param>
|
||||
public abstract void DrawLine(Pen pen, params PointF[] points);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a path outline in local coordinates using the given pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the outline fill path.</param>
|
||||
/// <param name="path">The path to stroke.</param>
|
||||
public abstract void Draw(Pen pen, IPath path);
|
||||
|
||||
/// <summary>
|
||||
/// Draws text onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="textOptions">The text rendering options.</param>
|
||||
/// <param name="text">The text to draw.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
RichTextOptions textOptions,
|
||||
ReadOnlySpan<char> text,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws text along a path baseline onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="textOptions">The text rendering options.</param>
|
||||
/// <param name="text">The text to draw.</param>
|
||||
/// <param name="path">The path used as the text baseline in local canvas coordinates.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
RichTextOptions textOptions,
|
||||
ReadOnlySpan<char> text,
|
||||
IPath path,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a prepared text block onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="textBlock">The prepared text block to draw.</param>
|
||||
/// <param name="location">The drawing location in local canvas coordinates.</param>
|
||||
/// <param name="wrappingLength">The wrapping length in pixels. Use <c>-1</c> to disable wrapping.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
TextBlock textBlock,
|
||||
PointF location,
|
||||
float wrappingLength,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a prepared text block along a path baseline onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="textBlock">The prepared text block to draw.</param>
|
||||
/// <param name="path">The path used as the text baseline in local canvas coordinates.</param>
|
||||
/// <param name="wrappingLength">The wrapping length in pixels. Use <c>-1</c> to disable wrapping.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
TextBlock textBlock,
|
||||
IPath path,
|
||||
float wrappingLength,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws one prepared line layout onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="lineLayout">The prepared line layout to draw.</param>
|
||||
/// <param name="location">The drawing location in local canvas coordinates.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
LineLayout lineLayout,
|
||||
PointF location,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws one prepared line layout along a path baseline onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="lineLayout">The prepared line layout to draw.</param>
|
||||
/// <param name="path">The path used as the text baseline in local canvas coordinates.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
LineLayout lineLayout,
|
||||
IPath path,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws layered glyph geometry.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to fill glyph layers.</param>
|
||||
/// <param name="pen">Pen used to outline dominant painted layers.</param>
|
||||
/// <param name="glyphs">Layered glyph geometry to draw.</param>
|
||||
public abstract void DrawGlyphs(
|
||||
Brush brush,
|
||||
Pen pen,
|
||||
IEnumerable<GlyphPathCollection> glyphs);
|
||||
|
||||
/// <summary>
|
||||
/// Measures the full set of layout metrics for the supplied text.
|
||||
/// </summary>
|
||||
/// <param name="textOptions">The text shaping and layout options.</param>
|
||||
/// <param name="text">The text to measure.</param>
|
||||
/// <returns>A <see cref="TextMetrics"/> value containing the metrics for the laid-out text.</returns>
|
||||
public abstract TextMetrics MeasureText(RichTextOptions textOptions, ReadOnlySpan<char> text);
|
||||
|
||||
/// <summary>
|
||||
/// Draws an image source region into a destination rectangle.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image.</param>
|
||||
/// <param name="sourceRect">The source rectangle within <paramref name="image"/>.</param>
|
||||
/// <param name="destinationRect">The destination rectangle in local canvas coordinates.</param>
|
||||
/// <param name="sampler">
|
||||
/// Optional resampler used when scaling or transforming the image. Defaults to <see cref="KnownResamplers.Bicubic"/>.
|
||||
/// </param>
|
||||
public abstract void DrawImage(
|
||||
Image image,
|
||||
Rectangle sourceRect,
|
||||
RectangleF destinationRect,
|
||||
IResampler? sampler = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a retained backend scene from the drawing commands currently queued on this canvas.
|
||||
/// </summary>
|
||||
/// <returns>A retained backend scene.</returns>
|
||||
public abstract DrawingBackendScene CreateScene();
|
||||
|
||||
/// <summary>
|
||||
/// Renders a retained backend scene into this canvas target.
|
||||
/// </summary>
|
||||
/// <param name="scene">The retained backend scene to render.</param>
|
||||
public abstract void RenderScene(DrawingBackendScene scene);
|
||||
|
||||
/// <summary>
|
||||
/// Seals queued drawing commands into the canvas timeline.
|
||||
/// </summary>
|
||||
public abstract void Flush();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Dispose();
|
||||
}
|
||||
}
|
||||
485
ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs
Normal file
485
ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs
Normal file
@ -0,0 +1,485 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using SixLabors.ImageSharp.Drawing.Processing.Backends;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Queues normalized composition commands emitted by <see cref="DrawingCanvas{TPixel}"/>
|
||||
/// and prepares them in deterministic draw order.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The batcher owns command buffering and replay ordering only; it does not rasterize or composite.
|
||||
/// Draw commands are stored in the command buffer until a timeline command-range entry references
|
||||
/// them. Existing retained scenes passed through <see cref="DrawingCanvas.RenderScene"/> are stored
|
||||
/// separately and referenced by timeline entry index. During disposal replay, command ranges are
|
||||
/// lowered to short-lived backend scenes at the position where the canvas recorded the range.
|
||||
/// </remarks>
|
||||
internal sealed class DrawingCanvasBatcher<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly Configuration configuration;
|
||||
|
||||
// Draw commands stay in this buffer until replay lowers referenced command ranges
|
||||
// into backend scenes at their recorded timeline position.
|
||||
private CompositionSceneCommand[] commands;
|
||||
private int commandCount;
|
||||
private int sealedCommandCount;
|
||||
|
||||
// Layer metadata is range-sensitive, so sealing advances this alongside command
|
||||
// sealing instead of letting layer state leak across later command ranges.
|
||||
private int layerCommandCount;
|
||||
private int sealedLayerCommandCount;
|
||||
|
||||
// Clip and dash flags gate whole-buffer command preparation; prepared commands
|
||||
// remain in the same command buffer until replay consumes it.
|
||||
private bool hasClips;
|
||||
private bool hasDashes;
|
||||
|
||||
// Timeline entries keep compact indexes into the command, barrier, and retained
|
||||
// scene buffers while preserving the order recorded by the canvas.
|
||||
private DrawingCanvasTimelineEntry[] entries;
|
||||
|
||||
// Apply barriers carry replay-time target read/process/write operations.
|
||||
private ApplyBarrier[] applyBarriers;
|
||||
private int applyBarrierCount;
|
||||
|
||||
// These are existing retained scenes recorded through RenderScene, not scenes
|
||||
// produced later from this batcher's own command ranges.
|
||||
private DrawingBackendScene[] insertedScenes;
|
||||
private int insertedSceneCount;
|
||||
|
||||
internal DrawingCanvasBatcher(Configuration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.commands = [];
|
||||
this.entries = [];
|
||||
this.applyBarriers = [];
|
||||
this.insertedScenes = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether there are queued commands or timeline entries.
|
||||
/// </summary>
|
||||
public bool HasRecordedWork => this.commandCount > 0 || this.TimelineEntryCount > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of ordered replay items recorded in the canvas timeline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not a draw-command count. A single entry can represent a contiguous command range,
|
||||
/// an apply barrier, or an inserted retained scene.
|
||||
/// </remarks>
|
||||
public int TimelineEntryCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Appends one normalized composition command to the pending queue.
|
||||
/// </summary>
|
||||
/// <param name="composition">The command to queue.</param>
|
||||
public void AddComposition(in CompositionCommand composition)
|
||||
{
|
||||
this.EnsureCommandCapacity(this.commandCount + 1);
|
||||
this.commands[this.commandCount++] = new PathCompositionSceneCommand(composition);
|
||||
|
||||
if (composition.Kind is not CompositionCommandKind.FillLayer)
|
||||
{
|
||||
this.layerCommandCount++;
|
||||
}
|
||||
|
||||
this.hasClips |= composition.ClipPaths is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one stroked path command to the pending queue.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to queue.</param>
|
||||
public void AddStrokePath(in StrokePathCommand command)
|
||||
{
|
||||
this.EnsureCommandCapacity(this.commandCount + 1);
|
||||
this.commands[this.commandCount++] = new StrokePathCompositionSceneCommand(command);
|
||||
this.hasClips |= command.ClipPaths is not null;
|
||||
this.hasDashes |= command.Pen.StrokePattern.Length >= 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one explicit stroked line-segment command to the pending queue.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to queue.</param>
|
||||
public void AddStrokeLineSegment(in StrokeLineSegmentCommand command)
|
||||
{
|
||||
this.EnsureCommandCapacity(this.commandCount + 1);
|
||||
this.commands[this.commandCount++] = new LineSegmentCompositionSceneCommand(command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one explicit stroked polyline command to the pending queue.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to queue.</param>
|
||||
public void AddStrokePolyline(in StrokePolylineCommand command)
|
||||
{
|
||||
this.EnsureCommandCapacity(this.commandCount + 1);
|
||||
this.commands[this.commandCount++] = new PolylineCompositionSceneCommand(command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seals currently queued commands into the replay timeline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This records a command range only. Backend scenes are created later by the replay path
|
||||
/// from the referenced command range, so sealing does not render or allocate backend scene state.
|
||||
/// </remarks>
|
||||
public void SealCommands()
|
||||
{
|
||||
int count = this.commandCount - this.sealedCommandCount;
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.EnsureEntryCapacity(this.TimelineEntryCount + 1);
|
||||
this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateCommandRange(
|
||||
this.sealedCommandCount,
|
||||
count,
|
||||
this.layerCommandCount != this.sealedLayerCommandCount);
|
||||
|
||||
this.sealedCommandCount = this.commandCount;
|
||||
this.sealedLayerCommandCount = this.layerCommandCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends an apply barrier to the replay timeline after sealing queued commands.
|
||||
/// </summary>
|
||||
/// <param name="barrier">The apply barrier to append.</param>
|
||||
internal void AddApplyBarrier(ApplyBarrier barrier)
|
||||
{
|
||||
this.SealCommands();
|
||||
this.EnsureApplyBarrierCapacity(this.applyBarrierCount + 1);
|
||||
|
||||
int barrierIndex = this.applyBarrierCount;
|
||||
this.applyBarriers[this.applyBarrierCount++] = barrier;
|
||||
this.EnsureEntryCapacity(this.TimelineEntryCount + 1);
|
||||
this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateApplyBarrier(barrierIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an existing retained scene in the replay timeline after sealing queued commands.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This stores only scenes passed to <see cref="DrawingCanvas.RenderScene"/>. Scenes produced
|
||||
/// from this canvas's own command ranges are created later by the backend from command batches.
|
||||
/// </remarks>
|
||||
/// <param name="scene">The retained scene to render at this point in the timeline.</param>
|
||||
public void AddScene(DrawingBackendScene scene)
|
||||
{
|
||||
this.SealCommands();
|
||||
this.EnsureInsertedSceneCapacity(this.insertedSceneCount + 1);
|
||||
|
||||
int sceneIndex = this.insertedSceneCount;
|
||||
this.insertedScenes[this.insertedSceneCount++] = scene;
|
||||
this.EnsureEntryCapacity(this.TimelineEntryCount + 1);
|
||||
this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateScene(sceneIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a retained backend scene from the recorded timeline.
|
||||
/// </summary>
|
||||
/// <param name="backend">The backend used to create the retained scene.</param>
|
||||
/// <param name="targetBounds">The target bounds used for target-dependent scene creation.</param>
|
||||
/// <param name="ownedResources">The resources that must stay alive for the returned scene.</param>
|
||||
/// <returns>The retained backend scene.</returns>
|
||||
public DrawingBackendScene CreateScene(
|
||||
IDrawingBackend backend,
|
||||
Rectangle targetBounds,
|
||||
IReadOnlyList<IDisposable>? ownedResources)
|
||||
{
|
||||
if (!this.HasRecordedWork)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot create a retained scene from an empty canvas.");
|
||||
}
|
||||
|
||||
this.SealAndPrepareCommands();
|
||||
|
||||
return backend.CreateScene(
|
||||
this.configuration,
|
||||
targetBounds,
|
||||
new DrawingCommandBatch(this.commands, this.commandCount, this.layerCommandCount > 0),
|
||||
ownedResources);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seals any pending commands and prepares queued command data for backend scene creation.
|
||||
/// </summary>
|
||||
public void SealAndPrepareCommands()
|
||||
{
|
||||
this.SealCommands();
|
||||
|
||||
this.PrepareCommands();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a command batch over one recorded command-range timeline entry.
|
||||
/// </summary>
|
||||
/// <param name="entry">The command-range timeline entry.</param>
|
||||
/// <returns>The command batch.</returns>
|
||||
public DrawingCommandBatch CreateCommandBatch(DrawingCanvasTimelineEntry entry)
|
||||
=> new(this.commands, entry.Index, entry.Count, entry.HasLayers);
|
||||
|
||||
/// <summary>
|
||||
/// Gets one recorded timeline entry.
|
||||
/// </summary>
|
||||
/// <param name="index">The entry index.</param>
|
||||
/// <returns>The recorded timeline entry.</returns>
|
||||
public DrawingCanvasTimelineEntry GetEntry(int index)
|
||||
=> this.entries[index];
|
||||
|
||||
/// <summary>
|
||||
/// Gets one recorded apply barrier.
|
||||
/// </summary>
|
||||
/// <param name="index">The apply-barrier index.</param>
|
||||
/// <returns>The recorded apply barrier.</returns>
|
||||
internal ApplyBarrier GetApplyBarrier(int index)
|
||||
=> this.applyBarriers[index];
|
||||
|
||||
/// <summary>
|
||||
/// Gets one retained scene reference recorded through <see cref="DrawingCanvas.RenderScene"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The retained-scene reference index.</param>
|
||||
/// <returns>The retained scene to render at the timeline entry.</returns>
|
||||
public DrawingBackendScene GetInsertedScene(int index)
|
||||
=> this.insertedScenes[index];
|
||||
|
||||
/// <summary>
|
||||
/// Clears command references after a prepared batch has been consumed.
|
||||
/// </summary>
|
||||
public void ClearCommandBatch()
|
||||
{
|
||||
Array.Clear(this.commands, 0, this.commandCount);
|
||||
Array.Clear(this.entries, 0, this.TimelineEntryCount);
|
||||
Array.Clear(this.applyBarriers, 0, this.applyBarrierCount);
|
||||
Array.Clear(this.insertedScenes, 0, this.insertedSceneCount);
|
||||
this.commandCount = 0;
|
||||
this.sealedCommandCount = 0;
|
||||
this.layerCommandCount = 0;
|
||||
this.sealedLayerCommandCount = 0;
|
||||
this.TimelineEntryCount = 0;
|
||||
this.applyBarrierCount = 0;
|
||||
this.insertedSceneCount = 0;
|
||||
this.hasClips = false;
|
||||
this.hasDashes = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the command buffer can store the requested command count without reallocating.
|
||||
/// </summary>
|
||||
/// <param name="requiredCapacity">The required command capacity.</param>
|
||||
private void EnsureCommandCapacity(int requiredCapacity)
|
||||
{
|
||||
if (requiredCapacity <= this.commands.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int nextCapacity = this.commands.Length == 0 ? 16 : this.commands.Length * 2;
|
||||
if (nextCapacity < requiredCapacity)
|
||||
{
|
||||
nextCapacity = requiredCapacity;
|
||||
}
|
||||
|
||||
Array.Resize(ref this.commands, nextCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the timeline entry buffer can store the requested entry count without reallocating.
|
||||
/// </summary>
|
||||
/// <param name="requiredCapacity">The required entry capacity.</param>
|
||||
private void EnsureEntryCapacity(int requiredCapacity)
|
||||
{
|
||||
if (requiredCapacity <= this.entries.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int nextCapacity = this.entries.Length == 0 ? 4 : this.entries.Length * 2;
|
||||
if (nextCapacity < requiredCapacity)
|
||||
{
|
||||
nextCapacity = requiredCapacity;
|
||||
}
|
||||
|
||||
Array.Resize(ref this.entries, nextCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the apply-barrier buffer can store the requested barrier count without reallocating.
|
||||
/// </summary>
|
||||
/// <param name="requiredCapacity">The required barrier capacity.</param>
|
||||
private void EnsureApplyBarrierCapacity(int requiredCapacity)
|
||||
{
|
||||
if (requiredCapacity <= this.applyBarriers.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int nextCapacity = this.applyBarriers.Length == 0 ? 2 : this.applyBarriers.Length * 2;
|
||||
if (nextCapacity < requiredCapacity)
|
||||
{
|
||||
nextCapacity = requiredCapacity;
|
||||
}
|
||||
|
||||
Array.Resize(ref this.applyBarriers, nextCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the inserted-scene buffer can store the requested scene count without reallocating.
|
||||
/// </summary>
|
||||
/// <param name="requiredCapacity">The required scene capacity.</param>
|
||||
private void EnsureInsertedSceneCapacity(int requiredCapacity)
|
||||
{
|
||||
if (requiredCapacity <= this.insertedScenes.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int nextCapacity = this.insertedScenes.Length == 0 ? 2 : this.insertedScenes.Length * 2;
|
||||
if (nextCapacity < requiredCapacity)
|
||||
{
|
||||
nextCapacity = requiredCapacity;
|
||||
}
|
||||
|
||||
Array.Resize(ref this.insertedScenes, nextCapacity);
|
||||
}
|
||||
|
||||
private void PrepareCommands()
|
||||
{
|
||||
if (!this.hasClips && !this.hasDashes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If clipping is present we need to apply that now before handing the command
|
||||
// to the backend. This avoids complicating the backend with clipping logic
|
||||
// and allows us to reuse the same optimized backend code for clipped and unclipped paths.
|
||||
int requestedParallelism = this.configuration.MaxDegreeOfParallelism;
|
||||
int partitionCount = ParallelExecutionHelper.GetPartitionCount(requestedParallelism, this.commandCount);
|
||||
|
||||
if (partitionCount <= 1)
|
||||
{
|
||||
for (int i = 0; i < this.commandCount; i++)
|
||||
{
|
||||
PrepareCommand(ref this.commands[i]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Parallel.For(
|
||||
0,
|
||||
partitionCount,
|
||||
ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, partitionCount),
|
||||
partitionIndex =>
|
||||
{
|
||||
// Integer division splits the commands into contiguous half-open ranges,
|
||||
// keeping the partitions balanced while assigning each command exactly once.
|
||||
int commandStart = (partitionIndex * this.commandCount) / partitionCount;
|
||||
int commandEnd = ((partitionIndex + 1) * this.commandCount) / partitionCount;
|
||||
|
||||
for (int i = commandStart; i < commandEnd; i++)
|
||||
{
|
||||
PrepareCommand(ref this.commands[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void PrepareCommand(ref CompositionSceneCommand command)
|
||||
{
|
||||
if (command is PathCompositionSceneCommand pathCommand)
|
||||
{
|
||||
CompositionCommand composition = pathCommand.Command;
|
||||
if (composition.ClipPaths is { Count: > 0 })
|
||||
{
|
||||
IPath path = composition.SourcePath;
|
||||
DrawingOptions sourceOptions = composition.DrawingOptions;
|
||||
|
||||
if (sourceOptions.Transform != Matrix4x4.Identity)
|
||||
{
|
||||
path = path.Transform(sourceOptions.Transform);
|
||||
}
|
||||
|
||||
path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths);
|
||||
|
||||
RasterizerOptions rasterizerOptions = composition.RasterizerOptions;
|
||||
DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions);
|
||||
|
||||
// Update the command with the clipped path.
|
||||
pathCommand.Command = CompositionCommand.Create(
|
||||
path,
|
||||
composition.Brush.Transform(sourceOptions.Transform),
|
||||
preparedOptions,
|
||||
in rasterizerOptions,
|
||||
composition.TargetBounds,
|
||||
composition.DestinationOffset,
|
||||
null,
|
||||
composition.IsInsideLayer);
|
||||
}
|
||||
}
|
||||
else if (command is StrokePathCompositionSceneCommand strokePathCommand)
|
||||
{
|
||||
StrokePathCommand composition = strokePathCommand.Command;
|
||||
|
||||
if (composition.ClipPaths is { Count: > 0 })
|
||||
{
|
||||
IPath path = composition.Pen.GeneratePath(composition.SourcePath);
|
||||
DrawingOptions sourceOptions = composition.DrawingOptions;
|
||||
|
||||
if (sourceOptions.Transform != Matrix4x4.Identity)
|
||||
{
|
||||
path = path.Transform(sourceOptions.Transform);
|
||||
}
|
||||
|
||||
path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths);
|
||||
|
||||
RasterizerOptions rasterizerOptions = composition.RasterizerOptions;
|
||||
DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions);
|
||||
|
||||
command = new PathCompositionSceneCommand(
|
||||
CompositionCommand.Create(
|
||||
path,
|
||||
composition.Brush.Transform(sourceOptions.Transform),
|
||||
preparedOptions,
|
||||
in rasterizerOptions,
|
||||
composition.TargetBounds,
|
||||
composition.DestinationOffset,
|
||||
null,
|
||||
composition.IsInsideLayer));
|
||||
}
|
||||
else
|
||||
{
|
||||
// We need to dash the path here before sending it to the backend.
|
||||
Pen pen = composition.Pen;
|
||||
if (pen.StrokePattern.Length >= 2)
|
||||
{
|
||||
strokePathCommand.Command = new StrokePathCommand(
|
||||
composition.SourcePath.GenerateDashes(pen.StrokeWidth, pen.StrokePattern.Span),
|
||||
composition.Brush,
|
||||
composition.DrawingOptions,
|
||||
composition.RasterizerOptions,
|
||||
composition.TargetBounds,
|
||||
composition.DestinationOffset,
|
||||
composition.Pen,
|
||||
null,
|
||||
composition.IsInsideLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DrawingOptions WithIdentityTransform(DrawingOptions source)
|
||||
=> source.Transform == Matrix4x4.Identity
|
||||
? source
|
||||
: new DrawingOptions(source.GraphicsOptions, source.ShapeOptions, Matrix4x4.Identity);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Advanced;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Extension methods for creating drawing canvas instances over ImageSharp image frames.
|
||||
/// </summary>
|
||||
public static class DrawingCanvasFactoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a drawing canvas over an existing typed image frame.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The caller owns the returned canvas and must dispose it to replay recorded work into the frame.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="frame">The frame backing the canvas.</param>
|
||||
/// <param name="configuration">The configuration to use for this canvas instance.</param>
|
||||
/// <param name="options">Initial drawing options for this canvas instance.</param>
|
||||
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
|
||||
/// <returns>A drawing canvas targeting <paramref name="frame"/>.</returns>
|
||||
public static DrawingCanvas CreateCanvas<TPixel>(
|
||||
this ImageFrame<TPixel> frame,
|
||||
Configuration configuration,
|
||||
DrawingOptions options,
|
||||
params IPath[] clipPaths)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
Guard.NotNull(frame, nameof(frame));
|
||||
Guard.NotNull(options, nameof(options));
|
||||
Guard.NotNull(clipPaths, nameof(clipPaths));
|
||||
|
||||
return new DrawingCanvas<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
frame.PixelBuffer.GetRegion(),
|
||||
clipPaths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a drawing canvas over an existing image frame.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The caller owns the returned canvas and must dispose it to replay recorded work into the frame.
|
||||
/// </remarks>
|
||||
/// <param name="frame">The frame backing the canvas.</param>
|
||||
/// <param name="configuration">The configuration to use for this canvas instance.</param>
|
||||
/// <param name="options">Initial drawing options for this canvas instance.</param>
|
||||
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
|
||||
/// <returns>A drawing canvas targeting <paramref name="frame"/>.</returns>
|
||||
public static DrawingCanvas CreateCanvas(
|
||||
this ImageFrame frame,
|
||||
Configuration configuration,
|
||||
DrawingOptions options,
|
||||
params IPath[] clipPaths)
|
||||
{
|
||||
Guard.NotNull(frame, nameof(frame));
|
||||
Guard.NotNull(options, nameof(options));
|
||||
Guard.NotNull(clipPaths, nameof(clipPaths));
|
||||
|
||||
CanvasFactoryVisitor visitor = new(configuration, options, clipPaths);
|
||||
frame.AcceptVisitor(visitor);
|
||||
return visitor.Value!;
|
||||
}
|
||||
|
||||
private struct CanvasFactoryVisitor : IImageFrameVisitor
|
||||
{
|
||||
private readonly Configuration configuration;
|
||||
private readonly DrawingOptions options;
|
||||
private readonly IPath[] clipPaths;
|
||||
|
||||
public CanvasFactoryVisitor(Configuration configuration, DrawingOptions options, IPath[] clipPaths)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.options = options;
|
||||
this.clipPaths = clipPaths;
|
||||
}
|
||||
|
||||
public DrawingCanvas? Value { get; private set; }
|
||||
|
||||
void IImageFrameVisitor.Visit<TPixel>(ImageFrame<TPixel> frame)
|
||||
=> this.Value = frame.CreateCanvas(this.configuration, this.options, this.clipPaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
ImageSharp.Drawing/Processing/DrawingCanvasState.cs
Normal file
65
ImageSharp.Drawing/Processing/DrawingCanvasState.cs
Normal file
@ -0,0 +1,65 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Immutable drawing state snapshot used by <see cref="DrawingCanvas{TPixel}"/>.
|
||||
/// </summary>
|
||||
internal sealed class DrawingCanvasState
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCanvasState"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options">Drawing options for this state.</param>
|
||||
/// <param name="clipPaths">Clip paths for this state.</param>
|
||||
/// <param name="targetBounds">Absolute target bounds used for commands recorded in this state.</param>
|
||||
/// <param name="destinationOffset">Absolute destination offset for paths recorded in local canvas coordinates.</param>
|
||||
public DrawingCanvasState(
|
||||
DrawingOptions options,
|
||||
IReadOnlyList<IPath> clipPaths,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset)
|
||||
{
|
||||
this.Options = options;
|
||||
this.ClipPaths = clipPaths;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets drawing options associated with this state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the original <see cref="DrawingOptions"/> reference supplied to the state.
|
||||
/// It is not deep-cloned.
|
||||
/// </remarks>
|
||||
public DrawingOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets clip paths associated with this state.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath> ClipPaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute target bounds used for commands recorded in this state.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset for paths recorded in local canvas coordinates.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this state represents a compositing layer.
|
||||
/// </summary>
|
||||
public bool IsLayer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layer compositing options when this state represents a compositing layer.
|
||||
/// </summary>
|
||||
public GraphicsOptions? LayerOptions { get; init; }
|
||||
}
|
||||
}
|
||||
94
ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs
Normal file
94
ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs
Normal file
@ -0,0 +1,94 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Identifies the kind of replay item stored in a drawing canvas timeline.
|
||||
/// </summary>
|
||||
internal enum DrawingCanvasTimelineEntryKind
|
||||
{
|
||||
/// <summary>
|
||||
/// A contiguous range of draw commands.
|
||||
/// </summary>
|
||||
CommandRange,
|
||||
|
||||
/// <summary>
|
||||
/// An apply barrier.
|
||||
/// </summary>
|
||||
ApplyBarrier,
|
||||
|
||||
/// <summary>
|
||||
/// An existing retained scene recorded through <see cref="DrawingCanvas.RenderScene"/>.
|
||||
/// </summary>
|
||||
Scene
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one ordered item in the canvas replay timeline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Command ranges reference contiguous draw commands; they are not backend scene objects yet.
|
||||
/// Apply barriers and retained scene references point into side buffers by index, keeping this
|
||||
/// type compact while preserving the exact order in which the canvas recorded replay work.
|
||||
/// </remarks>
|
||||
internal readonly struct DrawingCanvasTimelineEntry
|
||||
{
|
||||
private DrawingCanvasTimelineEntry(
|
||||
DrawingCanvasTimelineEntryKind kind,
|
||||
int index,
|
||||
int count,
|
||||
bool hasLayers)
|
||||
{
|
||||
this.Kind = kind;
|
||||
this.Index = index;
|
||||
this.Count = count;
|
||||
this.HasLayers = hasLayers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kind of replay item represented by this entry.
|
||||
/// </summary>
|
||||
public DrawingCanvasTimelineEntryKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command start index for command ranges, or the side-buffer index for barriers and scenes.
|
||||
/// </summary>
|
||||
public int Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of commands represented by a command-range entry.
|
||||
/// </summary>
|
||||
public int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command range contains layer boundary commands.
|
||||
/// </summary>
|
||||
public bool HasLayers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a command-range entry.
|
||||
/// </summary>
|
||||
/// <param name="startIndex">The first command index.</param>
|
||||
/// <param name="count">The command count.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command range contains layer boundary commands.</param>
|
||||
/// <returns>The command-range entry.</returns>
|
||||
public static DrawingCanvasTimelineEntry CreateCommandRange(int startIndex, int count, bool hasLayers)
|
||||
=> new(DrawingCanvasTimelineEntryKind.CommandRange, startIndex, count, hasLayers);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an apply-barrier entry.
|
||||
/// </summary>
|
||||
/// <param name="index">The apply-barrier index.</param>
|
||||
/// <returns>The apply-barrier entry.</returns>
|
||||
public static DrawingCanvasTimelineEntry CreateApplyBarrier(int index)
|
||||
=> new(DrawingCanvasTimelineEntryKind.ApplyBarrier, index, 0, false);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an entry for an existing retained scene recorded through <see cref="DrawingCanvas.RenderScene"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The retained-scene reference index.</param>
|
||||
/// <returns>The retained-scene entry.</returns>
|
||||
public static DrawingCanvasTimelineEntry CreateScene(int index)
|
||||
=> new(DrawingCanvasTimelineEntryKind.Scene, index, 0, false);
|
||||
}
|
||||
}
|
||||
1641
ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs
Normal file
1641
ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs
Normal file
File diff suppressed because it is too large
Load Diff
22
ImageSharp.Drawing/Processing/DrawingHelpers.cs
Normal file
22
ImageSharp.Drawing/Processing/DrawingHelpers.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
internal static class DrawingHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert a <see cref="DenseMatrix{Color}"/> to a <see cref="DenseMatrix{T}"/> of the given pixel type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
|
||||
/// <param name="colorMatrix">The color matrix.</param>
|
||||
public static DenseMatrix<TPixel> ToPixelMatrix<TPixel>(this DenseMatrix<Color> colorMatrix)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
DenseMatrix<TPixel> result = new(colorMatrix.Columns, colorMatrix.Rows);
|
||||
Color.ToPixel(colorMatrix.Span, result.Span);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
ImageSharp.Drawing/Processing/DrawingOperation.cs
Normal file
33
ImageSharp.Drawing/Processing/DrawingOperation.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
internal enum DrawingOperationKind : byte
|
||||
{
|
||||
Fill = 0,
|
||||
Draw = 1
|
||||
}
|
||||
|
||||
internal struct DrawingOperation
|
||||
{
|
||||
public DrawingOperationKind Kind { get; set; }
|
||||
|
||||
public IPath Path { get; set; }
|
||||
|
||||
public Point RenderLocation { get; set; }
|
||||
|
||||
public IntersectionRule IntersectionRule { get; set; }
|
||||
|
||||
public byte RenderPass { get; set; }
|
||||
|
||||
public Brush? Brush { get; set; }
|
||||
|
||||
public Pen? Pen { get; set; }
|
||||
|
||||
public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; set; }
|
||||
|
||||
public PixelColorBlendingMode PixelColorBlendingMode { get; set; }
|
||||
}
|
||||
}
|
||||
73
ImageSharp.Drawing/Processing/DrawingOptions.cs
Normal file
73
ImageSharp.Drawing/Processing/DrawingOptions.cs
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides options for influencing drawing operations, combining graphics rendering settings,
|
||||
/// shape fill-rule behavior, and an optional coordinate transform.
|
||||
/// </summary>
|
||||
public class DrawingOptions
|
||||
{
|
||||
private GraphicsOptions graphicsOptions;
|
||||
private ShapeOptions shapeOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingOptions"/> class.
|
||||
/// </summary>
|
||||
public DrawingOptions()
|
||||
{
|
||||
this.graphicsOptions = new GraphicsOptions();
|
||||
this.shapeOptions = new ShapeOptions();
|
||||
this.Transform = Matrix4x4.Identity;
|
||||
}
|
||||
|
||||
internal DrawingOptions(
|
||||
GraphicsOptions graphicsOptions,
|
||||
ShapeOptions shapeOptions,
|
||||
Matrix4x4 transform)
|
||||
{
|
||||
DebugGuard.NotNull(graphicsOptions, nameof(graphicsOptions));
|
||||
DebugGuard.NotNull(shapeOptions, nameof(shapeOptions));
|
||||
|
||||
this.graphicsOptions = graphicsOptions;
|
||||
this.shapeOptions = shapeOptions;
|
||||
this.Transform = transform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the graphics rendering options that control antialiasing, blending, alpha composition,
|
||||
/// and coverage thresholding for the drawing operation.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions
|
||||
{
|
||||
get => this.graphicsOptions;
|
||||
set
|
||||
{
|
||||
Guard.NotNull(value, nameof(this.GraphicsOptions));
|
||||
this.graphicsOptions = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the shape options that control fill-rule intersection mode and boolean clipping behavior.
|
||||
/// </summary>
|
||||
public ShapeOptions ShapeOptions
|
||||
{
|
||||
get => this.shapeOptions;
|
||||
set
|
||||
{
|
||||
Guard.NotNull(value, nameof(this.ShapeOptions));
|
||||
this.shapeOptions = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transform matrix applied to vector output before rasterization.
|
||||
/// For strokes, the pen is expanded in local geometry space and the resulting outline is transformed before rasterization.
|
||||
/// Defaults to <see cref="Matrix4x4.Identity"/>.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Adds extensions that help working with <see cref="DrawingOptions" />.
|
||||
/// </summary>
|
||||
public static class DrawingOptionsDefaultsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default drawing options against the source image processing context.
|
||||
/// </summary>
|
||||
/// <param name="context">The image processing context to retrieve defaults from.</param>
|
||||
/// <returns>The globally configured default options.</returns>
|
||||
public static DrawingOptions GetDrawingOptions(this IImageProcessingContext context)
|
||||
=> new(context.GetGraphicsOptions(), new ShapeOptions(), Matrix4x4.Identity);
|
||||
|
||||
/// <summary>
|
||||
/// Clones the path graphic options and applies changes required to force clearing.
|
||||
/// </summary>
|
||||
/// <param name="drawingOptions">The drawing options to clone</param>
|
||||
/// <returns>A clone of shapeOptions with ColorBlendingMode, AlphaCompositionMode, and BlendPercentage set</returns>
|
||||
internal static DrawingOptions CloneForClearOperation(this DrawingOptions drawingOptions)
|
||||
{
|
||||
GraphicsOptions options = drawingOptions.GraphicsOptions.DeepClone();
|
||||
options.ColorBlendingMode = PixelColorBlendingMode.Normal;
|
||||
options.AlphaCompositionMode = PixelAlphaCompositionMode.Src;
|
||||
options.BlendPercentage = 1F;
|
||||
|
||||
return new DrawingOptions(options, drawingOptions.ShapeOptions, drawingOptions.Transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
159
ImageSharp.Drawing/Processing/EllipticGradientBrush.cs
Normal file
159
ImageSharp.Drawing/Processing/EllipticGradientBrush.cs
Normal file
@ -0,0 +1,159 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides an implementation of a brush for painting elliptical gradients.
|
||||
/// The ellipse is defined by a center point, a point on the longest axis, and the ratio
|
||||
/// between the longest and shortest axes.
|
||||
/// </summary>
|
||||
public sealed class EllipticGradientBrush : GradientBrush
|
||||
{
|
||||
/// <inheritdoc cref="GradientBrush" />
|
||||
/// <param name="center">The center of the elliptical gradient and 0 for the color stops.</param>
|
||||
/// <param name="referenceAxisEnd">The end point of the reference axis of the ellipse.</param>
|
||||
/// <param name="axisRatio">
|
||||
/// The ratio of the axis widths.
|
||||
/// The second axis is perpendicular to the reference axis and its length is the reference axis length
|
||||
/// multiplied by this factor.
|
||||
/// </param>
|
||||
/// <param name="repetitionMode">Defines how the colors of the gradients are repeated.</param>
|
||||
/// <param name="colorStops">The color stops.</param>
|
||||
public EllipticGradientBrush(
|
||||
PointF center,
|
||||
PointF referenceAxisEnd,
|
||||
float axisRatio,
|
||||
GradientRepetitionMode repetitionMode,
|
||||
params ColorStop[] colorStops)
|
||||
: base(repetitionMode, colorStops)
|
||||
{
|
||||
this.Center = center;
|
||||
this.ReferenceAxisEnd = referenceAxisEnd;
|
||||
this.AxisRatio = axisRatio;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the center of the ellipse.
|
||||
/// </summary>
|
||||
public PointF Center { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point of the reference axis.
|
||||
/// </summary>
|
||||
public PointF ReferenceAxisEnd { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ratio of the secondary axis to the primary axis.
|
||||
/// </summary>
|
||||
public float AxisRatio { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Brush Transform(Matrix4x4 matrix)
|
||||
{
|
||||
PointF tc = PointF.Transform(this.Center, matrix);
|
||||
PointF tRef = PointF.Transform(this.ReferenceAxisEnd, matrix);
|
||||
|
||||
// Compute a point on the perpendicular (secondary) axis and transform it.
|
||||
float refDx = this.ReferenceAxisEnd.X - this.Center.X;
|
||||
float refDy = this.ReferenceAxisEnd.Y - this.Center.Y;
|
||||
float refLen = MathF.Sqrt((refDx * refDx) + (refDy * refDy));
|
||||
float secondLen = refLen * this.AxisRatio;
|
||||
|
||||
// Perpendicular direction (rotated 90 degrees).
|
||||
PointF secondEnd = new(
|
||||
this.Center.X + (-refDy / refLen * secondLen),
|
||||
this.Center.Y + (refDx / refLen * secondLen));
|
||||
PointF tSec = PointF.Transform(secondEnd, matrix);
|
||||
|
||||
// Derive new ratio from transformed lengths.
|
||||
float newRefLen = MathF.Sqrt(
|
||||
((tRef.X - tc.X) * (tRef.X - tc.X)) + ((tRef.Y - tc.Y) * (tRef.Y - tc.Y)));
|
||||
float newSecLen = MathF.Sqrt(
|
||||
((tSec.X - tc.X) * (tSec.X - tc.X)) + ((tSec.Y - tc.Y) * (tSec.Y - tc.Y)));
|
||||
float newRatio = newRefLen > 0f ? newSecLen / newRefLen : this.AxisRatio;
|
||||
|
||||
return new EllipticGradientBrush(tc, tRef, newRatio, this.RepetitionMode, this.ColorStopsArray);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region) =>
|
||||
new EllipticGradientBrushRenderer<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
canvasWidth,
|
||||
this,
|
||||
this.ColorStopsArray,
|
||||
this.RepetitionMode);
|
||||
|
||||
/// <inheritdoc />
|
||||
private sealed class EllipticGradientBrushRenderer<TPixel> : GradientBrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly PointF center;
|
||||
|
||||
private readonly float cosRotation;
|
||||
|
||||
private readonly float sinRotation;
|
||||
|
||||
private readonly float referenceRadiusSquared;
|
||||
|
||||
private readonly float secondRadiusSquared;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EllipticGradientBrushRenderer{TPixel}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="brush">The elliptic gradient brush.</param>
|
||||
/// <param name="colorStops">Definition of colors.</param>
|
||||
/// <param name="repetitionMode">Defines how the gradient colors are repeated.</param>
|
||||
public EllipticGradientBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
EllipticGradientBrush brush,
|
||||
ColorStop[] colorStops,
|
||||
GradientRepetitionMode repetitionMode)
|
||||
: base(configuration, options, canvasWidth, colorStops, repetitionMode)
|
||||
{
|
||||
this.center = brush.Center;
|
||||
|
||||
float refDx = brush.ReferenceAxisEnd.X - brush.Center.X;
|
||||
float refDy = brush.ReferenceAxisEnd.Y - brush.Center.Y;
|
||||
float rotation = MathF.Atan2(refDy, refDx);
|
||||
float referenceRadius = MathF.Sqrt((refDx * refDx) + (refDy * refDy));
|
||||
float secondRadius = referenceRadius * brush.AxisRatio;
|
||||
|
||||
this.referenceRadiusSquared = referenceRadius * referenceRadius;
|
||||
this.secondRadiusSquared = secondRadius * secondRadius;
|
||||
this.sinRotation = MathF.Sin(rotation);
|
||||
this.cosRotation = MathF.Cos(rotation);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override float PositionOnGradient(float x, float y)
|
||||
{
|
||||
float x0 = x - this.center.X;
|
||||
float y0 = y - this.center.Y;
|
||||
|
||||
float xR = (x0 * this.cosRotation) - (y0 * this.sinRotation);
|
||||
float yR = (x0 * this.sinRotation) + (y0 * this.cosRotation);
|
||||
|
||||
float xSquared = xR * xR;
|
||||
float ySquared = yR * yR;
|
||||
|
||||
return MathF.Sqrt((xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
244
ImageSharp.Drawing/Processing/GradientBrush.cs
Normal file
244
ImageSharp.Drawing/Processing/GradientBrush.cs
Normal file
@ -0,0 +1,244 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Base class for Gradient brushes
|
||||
/// </summary>
|
||||
public abstract class GradientBrush : Brush
|
||||
{
|
||||
/// <inheritdoc cref="Brush"/>
|
||||
/// <param name="repetitionMode">Defines how the colors are repeated beyond the interval [0..1]</param>
|
||||
/// <param name="colorStops">The gradient colors.</param>
|
||||
protected GradientBrush(GradientRepetitionMode repetitionMode, params ColorStop[] colorStops)
|
||||
{
|
||||
this.RepetitionMode = repetitionMode;
|
||||
|
||||
InsertionSort(colorStops, (a, b) => a.Ratio.CompareTo(b.Ratio));
|
||||
this.ColorStopsArray = colorStops;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets how the colors are repeated beyond the interval [0..1].
|
||||
/// </summary>
|
||||
public GradientRepetitionMode RepetitionMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color stops for this gradient.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<ColorStop> ColorStops => this.ColorStopsArray;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color stops array for use by derived applicators.
|
||||
/// </summary>
|
||||
protected ColorStop[] ColorStopsArray { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is GradientBrush brush)
|
||||
{
|
||||
return this.RepetitionMode == brush.RepetitionMode
|
||||
&& this.ColorStopsArray?.SequenceEqual(brush.ColorStopsArray) == true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(this.RepetitionMode, this.ColorStopsArray);
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the collection in place using a stable insertion sort.
|
||||
/// <see cref="Array.Sort{T}(T[], Comparison{T})"/> is not stable and can reorder
|
||||
/// equal-ratio color stops, producing non-deterministic gradient results.
|
||||
/// </summary>
|
||||
private static void InsertionSort<T>(T[] collection, Comparison<T> comparison)
|
||||
{
|
||||
int count = collection.Length;
|
||||
for (int j = 1; j < count; j++)
|
||||
{
|
||||
T key = collection[j];
|
||||
|
||||
int i = j - 1;
|
||||
for (; i >= 0 && comparison(collection[i], key) > 0; i--)
|
||||
{
|
||||
collection[i + 1] = collection[i];
|
||||
}
|
||||
|
||||
collection[i + 1] = key;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for gradient brush applicators
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
internal abstract class GradientBrushRenderer<TPixel> : BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private static readonly TPixel Transparent = Color.Transparent.ToPixel<TPixel>();
|
||||
|
||||
private readonly ColorStop[] colorStops;
|
||||
|
||||
private readonly GradientRepetitionMode repetitionMode;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GradientBrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="colorStops">An array of color stops sorted by their position.</param>
|
||||
/// <param name="repetitionMode">Defines if and how the gradient should be repeated.</param>
|
||||
protected GradientBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
ColorStop[] colorStops,
|
||||
GradientRepetitionMode repetitionMode)
|
||||
: base(configuration, options, canvasWidth)
|
||||
{
|
||||
this.colorStops = colorStops;
|
||||
this.repetitionMode = repetitionMode;
|
||||
}
|
||||
|
||||
internal TPixel this[int x, int y]
|
||||
{
|
||||
get
|
||||
{
|
||||
float fx = x + 0.5f;
|
||||
float fy = y + 0.5f;
|
||||
|
||||
float positionOnCompleteGradient = this.PositionOnGradient(fx, fy);
|
||||
if (float.IsNaN(positionOnCompleteGradient))
|
||||
{
|
||||
return Transparent;
|
||||
}
|
||||
|
||||
switch (this.repetitionMode)
|
||||
{
|
||||
case GradientRepetitionMode.Repeat:
|
||||
positionOnCompleteGradient %= 1;
|
||||
break;
|
||||
case GradientRepetitionMode.Reflect:
|
||||
positionOnCompleteGradient %= 2;
|
||||
if (positionOnCompleteGradient > 1)
|
||||
{
|
||||
positionOnCompleteGradient = 2 - positionOnCompleteGradient;
|
||||
}
|
||||
|
||||
break;
|
||||
case GradientRepetitionMode.DontFill:
|
||||
if (positionOnCompleteGradient is > 1 or < 0)
|
||||
{
|
||||
return Transparent;
|
||||
}
|
||||
|
||||
break;
|
||||
case GradientRepetitionMode.None:
|
||||
default:
|
||||
// do nothing. The following could be done, but is not necessary:
|
||||
// onLocalGradient = Math.Min(0, Math.Max(1, onLocalGradient));
|
||||
break;
|
||||
}
|
||||
|
||||
(ColorStop from, ColorStop to) = this.GetGradientSegment(positionOnCompleteGradient);
|
||||
|
||||
if (from.Color.Equals(to.Color))
|
||||
{
|
||||
return from.Color.ToPixel<TPixel>();
|
||||
}
|
||||
|
||||
float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio);
|
||||
|
||||
// TODO: This should use premultiplied vectors to avoid bad blends e.g. red -> brown <- green.
|
||||
return Color.FromScaledVector(
|
||||
Vector4.Lerp(
|
||||
from.Color.ToScaledVector4(),
|
||||
to.Color.ToScaledVector4(),
|
||||
onLocalGradient)).ToPixel<TPixel>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace)
|
||||
{
|
||||
Span<float> amounts = workspace.GetAmounts(scanline.Length);
|
||||
Span<TPixel> overlays = workspace.GetOverlays(scanline.Length);
|
||||
float blendPercentage = this.Options.BlendPercentage;
|
||||
|
||||
// TODO: Remove bounds checks.
|
||||
if (blendPercentage < 1)
|
||||
{
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amounts[i] = scanline[i] * blendPercentage;
|
||||
overlays[i] = this[x + i, y];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amounts[i] = scanline[i];
|
||||
overlays[i] = this[x + i, y];
|
||||
}
|
||||
}
|
||||
|
||||
this.Blender.Blend(
|
||||
this.Configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
overlays,
|
||||
amounts,
|
||||
workspace.GetBlendScratch(scanline.Length, 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the position on the gradient for a given point.
|
||||
/// This method is abstract as it's content depends on the shape of the gradient.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the point.</param>
|
||||
/// <param name="y">The y-coordinate of the point.</param>
|
||||
/// <returns>
|
||||
/// The position the given point has on the gradient.
|
||||
/// The position is not bound to the [0..1] interval.
|
||||
/// Values outside of that interval may be treated differently,
|
||||
/// e.g. for the <see cref="GradientRepetitionMode" /> enum.
|
||||
/// </returns>
|
||||
protected abstract float PositionOnGradient(float x, float y);
|
||||
|
||||
private (ColorStop From, ColorStop To) GetGradientSegment(float positionOnCompleteGradient)
|
||||
{
|
||||
ColorStop localGradientFrom = this.colorStops[0];
|
||||
ColorStop localGradientTo = default;
|
||||
|
||||
foreach (ColorStop colorStop in this.colorStops)
|
||||
{
|
||||
localGradientTo = colorStop;
|
||||
|
||||
if (colorStop.Ratio > positionOnCompleteGradient)
|
||||
{
|
||||
// we're done here, so break it!
|
||||
break;
|
||||
}
|
||||
|
||||
localGradientFrom = localGradientTo;
|
||||
}
|
||||
|
||||
return (localGradientFrom, localGradientTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
ImageSharp.Drawing/Processing/GradientRepetitionMode.cs
Normal file
35
ImageSharp.Drawing/Processing/GradientRepetitionMode.cs
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Modes to repeat a gradient.
|
||||
/// </summary>
|
||||
public enum GradientRepetitionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Don't repeat, keep the color of start and end beyond those points stable.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Repeat the gradient.
|
||||
/// If it's a black-white gradient, with Repeat it will be Black->{gray}->White|Black->{gray}->White|...
|
||||
/// </summary>
|
||||
Repeat,
|
||||
|
||||
/// <summary>
|
||||
/// Reflect the gradient.
|
||||
/// Similar to <see cref="Repeat"/>, but each other repetition uses inverse order of <see cref="ColorStop"/>s.
|
||||
/// Used on a Black-White gradient, Reflect leads to Black->{gray}->White->{gray}->White...
|
||||
/// </summary>
|
||||
Reflect,
|
||||
|
||||
/// <summary>
|
||||
/// With DontFill a gradient does not touch any pixel beyond it's borders.
|
||||
/// For the <see cref="LinearGradientBrush"/> this is beyond the orthogonal through start and end,
|
||||
/// For <see cref="RadialGradientBrush" /> and <see cref="EllipticGradientBrush" /> it's beyond 1.0.
|
||||
/// </summary>
|
||||
DontFill
|
||||
}
|
||||
}
|
||||
268
ImageSharp.Drawing/Processing/ImageBrush.cs
Normal file
268
ImageSharp.Drawing/Processing/ImageBrush.cs
Normal file
@ -0,0 +1,268 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides an implementation of an image brush for painting images within areas.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format of the source image.</typeparam>
|
||||
public sealed class ImageBrush<TPixel> : ImageBrush
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
public ImageBrush(Image<TPixel> image)
|
||||
: base(image)
|
||||
=> this.SourceImage = image;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
/// <param name="offset">An offset to apply to the image while drawing the texture.</param>
|
||||
public ImageBrush(Image<TPixel> image, Point offset)
|
||||
: base(image, offset)
|
||||
=> this.SourceImage = image;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
/// <param name="region">The region of interest within the source image.</param>
|
||||
public ImageBrush(Image<TPixel> image, RectangleF region)
|
||||
: base(image, region)
|
||||
=> this.SourceImage = image;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
/// <param name="region">The region of interest within the source image.</param>
|
||||
/// <param name="offset">An offset to apply to the image while drawing the texture.</param>
|
||||
public ImageBrush(Image<TPixel> image, RectangleF region, Point offset)
|
||||
: base(image, region, offset)
|
||||
=> this.SourceImage = image;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the typed source image used by this brush.
|
||||
/// </summary>
|
||||
public Image<TPixel> SourceImage { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The untyped base class for image brushes, used to support non-generic brush references in drawing contexts.
|
||||
/// </summary>
|
||||
public abstract class ImageBrush : Brush
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
protected ImageBrush(Image image)
|
||||
: this(image, image.Bounds)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="offset">
|
||||
/// An offset to apply the to image image while drawing apply the texture.
|
||||
/// </param>
|
||||
protected ImageBrush(Image image, Point offset)
|
||||
: this(image, image.Bounds, offset)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="region">
|
||||
/// The region of interest.
|
||||
/// This overrides any region used to initialize the brush applicator.
|
||||
/// </param>
|
||||
protected ImageBrush(Image image, RectangleF region)
|
||||
: this(image, region, Point.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="region">
|
||||
/// The region of interest.
|
||||
/// This overrides any region used to initialize the brush applicator.
|
||||
/// </param>
|
||||
/// <param name="offset">
|
||||
/// An offset to apply the to image image while drawing apply the texture.
|
||||
/// </param>
|
||||
protected ImageBrush(Image image, RectangleF region, Point offset)
|
||||
{
|
||||
this.UntypedImage = image;
|
||||
this.SourceRegion = RectangleF.Intersect(image.Bounds, region);
|
||||
this.Offset = offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source image used by this brush.
|
||||
/// </summary>
|
||||
public Image UntypedImage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source region within the image.
|
||||
/// </summary>
|
||||
public RectangleF SourceRegion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offset applied to the brush origin.
|
||||
/// </summary>
|
||||
public Point Offset { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is ImageBrush ib)
|
||||
{
|
||||
return ib.UntypedImage == this.UntypedImage && ib.SourceRegion == this.SourceRegion;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => HashCode.Combine(this.UntypedImage, this.SourceRegion);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
{
|
||||
if (this.UntypedImage is Image<TPixel> image)
|
||||
{
|
||||
return new ImageBrushRenderer<TPixel>(configuration, options, canvasWidth, image, region, this.SourceRegion, this.Offset);
|
||||
}
|
||||
|
||||
// This will never be hit as the brush is always normalized by the drawing canvas
|
||||
// but we do it to satisfy the type system.
|
||||
ThrowIfInvalidImagePixelFormat();
|
||||
return null;
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowIfInvalidImagePixelFormat()
|
||||
=> throw new UnreachableException("The pixel format of the image is not supported by this brush renderer");
|
||||
|
||||
/// <summary>
|
||||
/// The image brush applicator.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class ImageBrushRenderer<TPixel> : BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly ImageFrame<TPixel> sourceFrame;
|
||||
|
||||
/// <summary>
|
||||
/// The region of the source image we will be using to draw from.
|
||||
/// </summary>
|
||||
private readonly Rectangle sourceRegion;
|
||||
|
||||
/// <summary>
|
||||
/// The Y offset.
|
||||
/// </summary>
|
||||
private readonly int offsetY;
|
||||
|
||||
/// <summary>
|
||||
/// The X offset.
|
||||
/// </summary>
|
||||
private readonly int offsetX;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="targetRegion">The region of the target image we will be drawing to.</param>
|
||||
/// <param name="sourceRegion">The region of the source image we will be using to source pixels to draw from.</param>
|
||||
/// <param name="offset">An offset to apply to the texture while drawing.</param>
|
||||
public ImageBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
Image<TPixel> image,
|
||||
RectangleF targetRegion,
|
||||
RectangleF sourceRegion,
|
||||
Point offset)
|
||||
: base(configuration, options, canvasWidth)
|
||||
{
|
||||
this.sourceFrame = image.Frames.RootFrame;
|
||||
this.sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)sourceRegion);
|
||||
|
||||
this.offsetY = (int)MathF.Floor(targetRegion.Top) + offset.Y;
|
||||
this.offsetX = (int)MathF.Floor(targetRegion.Left) + offset.X;
|
||||
}
|
||||
|
||||
internal TPixel this[int x, int y]
|
||||
{
|
||||
get
|
||||
{
|
||||
int srcX = ((x - this.offsetX) % this.sourceRegion.Width) + this.sourceRegion.X;
|
||||
int srcY = ((y - this.offsetY) % this.sourceRegion.Height) + this.sourceRegion.Y;
|
||||
return this.sourceFrame[srcX, srcY];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace)
|
||||
{
|
||||
Span<float> amountSpan = workspace.GetAmounts(scanline.Length);
|
||||
Span<TPixel> overlaySpan = workspace.GetOverlays(scanline.Length);
|
||||
|
||||
int offsetX = x - this.offsetX;
|
||||
int sourceY = ((((y - this.offsetY) % this.sourceRegion.Height) // clamp the number between -height and +height
|
||||
+ this.sourceRegion.Height) % this.sourceRegion.Height) // clamp the number between 0 and +height
|
||||
+ this.sourceRegion.Y;
|
||||
Span<TPixel> sourceRow = this.sourceFrame.PixelBuffer.DangerousGetRowSpan(sourceY);
|
||||
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amountSpan[i] = scanline[i] * this.Options.BlendPercentage;
|
||||
|
||||
int sourceX = ((((i + offsetX) % this.sourceRegion.Width) // clamp the number between -width and +width
|
||||
+ this.sourceRegion.Width) % this.sourceRegion.Width) // clamp the number between 0 and +width
|
||||
+ this.sourceRegion.X;
|
||||
|
||||
overlaySpan[i] = sourceRow[sourceX];
|
||||
}
|
||||
|
||||
this.Blender.Blend(
|
||||
this.Configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
overlaySpan,
|
||||
amountSpan,
|
||||
workspace.GetBlendScratch(scanline.Length, 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
ImageSharp.Drawing/Processing/LinearGradientBrush.cs
Normal file
199
ImageSharp.Drawing/Processing/LinearGradientBrush.cs
Normal file
@ -0,0 +1,199 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides a brush that paints linear gradients within an area.
|
||||
/// Supports both classic two-point gradients and three-point (rotated) gradients.
|
||||
/// </summary>
|
||||
public sealed class LinearGradientBrush : GradientBrush
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearGradientBrush"/> class using
|
||||
/// a start and end point.
|
||||
/// </summary>
|
||||
/// <param name="p0">The start point of the gradient.</param>
|
||||
/// <param name="p1">The end point of the gradient.</param>
|
||||
/// <param name="repetitionMode">Defines how the colors are repeated.</param>
|
||||
/// <param name="colorStops">The ordered color stops of the gradient.</param>
|
||||
public LinearGradientBrush(
|
||||
PointF p0,
|
||||
PointF p1,
|
||||
GradientRepetitionMode repetitionMode,
|
||||
params ColorStop[] colorStops)
|
||||
: base(repetitionMode, colorStops)
|
||||
{
|
||||
this.StartPoint = p0;
|
||||
this.EndPoint = p1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearGradientBrush"/> class using
|
||||
/// three points to define a rotated gradient axis.
|
||||
/// </summary>
|
||||
/// <param name="p0">The first point (start of the gradient).</param>
|
||||
/// <param name="p1">The second point (gradient vector endpoint).</param>
|
||||
/// <param name="rotationPoint">
|
||||
/// The rotation reference point. This defines the rotation of the gradient axis.
|
||||
/// </param>
|
||||
/// <param name="repetitionMode">Defines how the colors are repeated.</param>
|
||||
/// <param name="colorStops">The ordered color stops of the gradient.</param>
|
||||
public LinearGradientBrush(
|
||||
PointF p0,
|
||||
PointF p1,
|
||||
PointF rotationPoint,
|
||||
GradientRepetitionMode repetitionMode,
|
||||
params ColorStop[] colorStops)
|
||||
: base(repetitionMode, colorStops)
|
||||
{
|
||||
ResolveAxis(p0, p1, rotationPoint, out PointF start, out PointF end);
|
||||
this.StartPoint = start;
|
||||
this.EndPoint = end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start point of the gradient axis.
|
||||
/// </summary>
|
||||
public PointF StartPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point of the gradient axis.
|
||||
/// </summary>
|
||||
public PointF EndPoint { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Brush Transform(Matrix4x4 matrix)
|
||||
=> new LinearGradientBrush(
|
||||
PointF.Transform(this.StartPoint, matrix),
|
||||
PointF.Transform(this.EndPoint, matrix),
|
||||
this.RepetitionMode,
|
||||
this.ColorStopsArray);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is LinearGradientBrush brush)
|
||||
{
|
||||
return base.Equals(other)
|
||||
&& this.StartPoint.Equals(brush.StartPoint)
|
||||
&& this.EndPoint.Equals(brush.EndPoint);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(base.GetHashCode(), this.StartPoint, this.EndPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a three-point gradient axis into a two-point axis by projecting
|
||||
/// the gradient vector (p0 to p1) onto the perpendicular of the rotation vector (p0 to rotationPoint).
|
||||
/// This follows the COLRv1 font specification for rotated linear gradients.
|
||||
/// </summary>
|
||||
/// <param name="p0">The gradient start point.</param>
|
||||
/// <param name="p1">The gradient vector endpoint.</param>
|
||||
/// <param name="rotationPoint">The rotation reference point.</param>
|
||||
/// <param name="start">The resolved start point of the gradient axis.</param>
|
||||
/// <param name="end">The resolved end point of the gradient axis.</param>
|
||||
private static void ResolveAxis(PointF p0, PointF p1, PointF rotationPoint, out PointF start, out PointF end)
|
||||
{
|
||||
// Gradient vector from p0 to p1.
|
||||
float vx = p1.X - p0.X;
|
||||
float vy = p1.Y - p0.Y;
|
||||
|
||||
// Rotation vector from p0 to rotation point.
|
||||
float rx = rotationPoint.X - p0.X;
|
||||
float ry = rotationPoint.Y - p0.Y;
|
||||
|
||||
// Perpendicular to the rotation vector.
|
||||
float nx = ry;
|
||||
float ny = -rx;
|
||||
|
||||
float ndotn = (nx * nx) + (ny * ny);
|
||||
if (ndotn == 0f)
|
||||
{
|
||||
// Degenerate: p0 == rotationPoint, fall back to original axis.
|
||||
start = p0;
|
||||
end = p1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Project the gradient vector onto the perpendicular direction.
|
||||
float vdotn = (vx * nx) + (vy * ny);
|
||||
float scale = vdotn / ndotn;
|
||||
start = p0;
|
||||
end = new PointF(p0.X + (scale * nx), p0.Y + (scale * ny));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
=> new LinearGradientBrushRenderer<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
canvasWidth,
|
||||
this,
|
||||
this.ColorStopsArray,
|
||||
this.RepetitionMode);
|
||||
|
||||
/// <summary>
|
||||
/// Implements the gradient application logic for <see cref="LinearGradientBrush"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class LinearGradientBrushRenderer<TPixel> : GradientBrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly PointF start;
|
||||
private readonly float alongX;
|
||||
private readonly float alongY;
|
||||
private readonly float alongsSquared;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearGradientBrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The ImageSharp configuration.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="brush">The linear gradient brush.</param>
|
||||
/// <param name="colorStops">The gradient color stops.</param>
|
||||
/// <param name="repetitionMode">Defines how the gradient repeats.</param>
|
||||
public LinearGradientBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
LinearGradientBrush brush,
|
||||
ColorStop[] colorStops,
|
||||
GradientRepetitionMode repetitionMode)
|
||||
: base(configuration, options, canvasWidth, colorStops, repetitionMode)
|
||||
{
|
||||
this.start = brush.StartPoint;
|
||||
|
||||
this.alongX = brush.EndPoint.X - this.start.X;
|
||||
this.alongY = brush.EndPoint.Y - this.start.Y;
|
||||
this.alongsSquared = (this.alongX * this.alongX) + (this.alongY * this.alongY);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override float PositionOnGradient(float x, float y)
|
||||
{
|
||||
if (this.alongsSquared == 0f)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
float deltaX = x - this.start.X;
|
||||
float deltaY = y - this.start.Y;
|
||||
return ((deltaX * this.alongX) + (deltaY * this.alongY)) / this.alongsSquared;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user