From f95d003a7344c278edf575a00cd203e964c6a990 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 15:32:09 -0300 Subject: [PATCH 001/217] add Matrix class --- src/Common/Drawing2D/Matrix.cs | 378 ++++++++++++++++++++++++ src/Common/Drawing2D/MatrixOrder.cs | 17 ++ src/Common/Point.cs | 2 +- src/Common/PointF.cs | 2 +- test/Common/Drawing2D/MatrixUnitTest.cs | 208 +++++++++++++ 5 files changed, 605 insertions(+), 2 deletions(-) create mode 100644 src/Common/Drawing2D/Matrix.cs create mode 100644 src/Common/Drawing2D/MatrixOrder.cs create mode 100644 test/Common/Drawing2D/MatrixUnitTest.cs diff --git a/src/Common/Drawing2D/Matrix.cs b/src/Common/Drawing2D/Matrix.cs new file mode 100644 index 0000000..19145c6 --- /dev/null +++ b/src/Common/Drawing2D/Matrix.cs @@ -0,0 +1,378 @@ + +using System; +using System.Numerics; +using SkiaSharp; + +namespace GeneXus.Drawing.Drawing2D; + +public sealed class Matrix : ICloneable, IDisposable +{ + internal SKMatrix m_matrix; + + internal Matrix(SKMatrix matrix) + { + m_matrix = matrix; + } + + /// + /// Initializes a new instance of the class as the identity matrix. + /// + public Matrix() + : this(SKMatrix.CreateIdentity()) { } + + /// + /// Initializes a new instance of the class with the specified elements. + /// + public Matrix(float m11, float m12, float m21, float m22, float dx, float dy) + : this(CreateElementsMatrix(m11, m12, m21, m22, dx, dy)) { } + + /// + /// Constructs a utilizing the specified matrix. + /// + /// + public Matrix(Matrix3x2 matrix) + : this(matrix.M11, matrix.M12, matrix.M21, matrix.M22, matrix.M31, matrix.M32) { } + + /// + /// Initializes a new instance of the class to the geometric transform + /// defined by the specified and array of . + /// + public Matrix(RectangleF rect, PointF[] plgpts) + : this(CreateTransformMatrix( + new PointF[] + { + new(rect.Left, rect.Top), + new(rect.Right, rect.Top), + new(rect.Left, rect.Bottom) + }, + plgpts)) + { } + + /// + /// Initializes a new instance of the class to the geometric transform + /// defined by the specified and array of . + /// + public Matrix(Rectangle rect, Point[] plgpts) + : this(new RectangleF(rect.m_rect), Array.ConvertAll(plgpts, point => new PointF(point.m_point))) { } + + /// + /// Cleans up resources for this . + /// + ~Matrix() => Dispose(false); + + + #region IDisposble + + /// + /// Cleans up resources for this . + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + private void Dispose(bool disposing) { } + + #endregion + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public object Clone() + => new Matrix(m_matrix); + + #endregion + + + #region IEqualitable + + /// + /// Tests whether the specified object is a and is identical to this . + /// + public override bool Equals(object obj) + => obj is Matrix matrix && m_matrix.Equals(matrix.m_matrix); + + /// + /// Get the has code of this . + /// + public override int GetHashCode() + => m_matrix.GetHashCode(); + + #endregion + + + #region Operators + + /// + /// Creates a with specified . + /// + public static explicit operator SKMatrix(Matrix matrix) => matrix.m_matrix; + + /// + /// Tests whether two objects are identical. + /// + public static bool operator ==(Matrix left, Matrix right) => left.m_matrix == right.m_matrix; + + /// + /// Tests whether two objects are different. + /// + public static bool operator !=(Matrix left, Matrix right) => left.m_matrix != right.m_matrix; + + #endregion + + + #region Properties + + /// + /// Gets an array of floating-point values that represents the elements of this . + /// + public float[] Elements => new float[] + { + m_matrix.ScaleX, m_matrix.SkewX, + m_matrix.SkewY, m_matrix.ScaleY, + m_matrix.TransX, m_matrix.TransY + }; + + /// + /// Gets a value indicating whether this is the identity matrix. + /// + public bool IsIdentity => m_matrix.IsIdentity; + + /// + /// Gets a value indicating whether this is invertible. + /// + public bool IsInvertible => m_matrix.IsInvertible; + + /// + /// Gets or sets the elements for the matrix. + /// + public Matrix3x2 MatrixElements + { + get => new( + m_matrix.ScaleX, + m_matrix.SkewX, + m_matrix.SkewY, + m_matrix.ScaleY, + m_matrix.TransX, + m_matrix.TransY + ); + set + { + m_matrix.ScaleX = value.M11; + m_matrix.SkewX = value.M12; + m_matrix.SkewY = value.M21; + m_matrix.ScaleY = value.M22; + m_matrix.TransX = value.M31; + m_matrix.TransY = value.M32; + m_matrix.Persp0 = 0; + m_matrix.Persp1 = 0; + m_matrix.Persp2 = 1; + } + } + + /// + /// Gets the x translation value (the dx value, or the element in the third row and first column) + /// of this . + /// + public float OffsetX => m_matrix.TransX; + + /// + /// Gets the y translation value (the dy value, or the element in the third row and second column) + /// of this . + /// + public float OffsetY => m_matrix.TransY; + + #endregion + + + #region Methods + + /// + /// Inverts this , if it is invertible. + /// + public void Invert() + => m_matrix = Invert(m_matrix); + + /// + /// Multiplies this by the matrix specified in the parameter, + /// and in the order specified in the parameter. + /// + private void Multiply(SKMatrix matrix, MatrixOrder order = MatrixOrder.Prepend) + => m_matrix = order == MatrixOrder.Prepend ? Multiply(matrix, m_matrix) : Multiply(m_matrix, matrix); + + /// + /// Multiplies this by the matrix specified in the parameter, + /// and in the order specified in the parameter. + /// + public void Multiply(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend) + => Multiply(matrix.m_matrix, order); + + /// + /// Resets this to have the elements of the identity matrix. + /// + public void Reset() + => m_matrix = SKMatrix.CreateIdentity(); + + /// + /// Applies a clockwise rotation of the specified angle about the origin to this . + /// + public void Rotate(float angle, MatrixOrder order = MatrixOrder.Prepend) + => Multiply(SKMatrix.CreateRotationDegrees(-angle), order); + + /// + /// Applies a clockwise rotation about the specified point to this . + /// + public void RotateAt(float angle, PointF point, MatrixOrder order = MatrixOrder.Prepend) + { + Translate(point.X, point.Y, order); + Rotate(angle, order); + Translate(-point.X, -point.Y, order); + } + + /// + /// Applies a clockwise rotation about the specified point to this . + /// + public void RotateAt(float angle, Point point, MatrixOrder order = MatrixOrder.Prepend) + => RotateAt(angle, new PointF(point.m_point), order); + + /// + /// Applies the specified scale vector to this by prepending the scale vector. + /// + public void Scale(float scaleX, float scaleY, MatrixOrder order = MatrixOrder.Prepend) + => Multiply(SKMatrix.CreateScale(scaleX, scaleY), order); + + /// + /// Applies the specified shear vector to this by prepending the shear vector. + /// + public void Shear(float shearX, float shearY, MatrixOrder order = MatrixOrder.Prepend) + => Multiply(SKMatrix.CreateSkew(shearY, shearX), order); + + /// + /// Applies the specified translation vector to this by prepending the translation vector. + /// + public void Translate(float offsetX, float offsetY, MatrixOrder order = MatrixOrder.Prepend) + => Multiply(SKMatrix.CreateTranslation(offsetX, offsetY), order); + + /// + /// Applies the geometric transform represented by this to a specified + /// array of . + /// + public void TransformPoints(PointF[] points) + => TransformPoints(points, m_matrix, p => new(p), p => p.m_point); + + /// + /// Applies the geometric transform represented by this to a specified + /// array of . + /// + public void TransformPoints(Point[] points) + => TransformPoints(points, m_matrix, p => new(p), p => p.m_point); + + /// + /// Applies only the scale and rotate components of this to the specified + /// array of . + /// + public void TransformVectors(Point[] points) + => TransformVectors(points, p => new(p), p => p.m_point); + + /// + /// Multiplies each vector in an array by this . The translation elements of + /// this matrix (third row) are ignored. + /// + public void VectorTransformPoints(Point[] points) + => TransformVectors(points); + + #endregion + + + #region Utilities + + private const float DEG_OFFSET = -90; + + /* + * NOTE: SkiaSharp and System.Drawing have a different + * representation for transformation matrices + * + * SkiaSharp: System.Drawing: + * ┌ ┐ ┌ ┐ + * │ ScaleX SkewX TransX │ │ ScaleX SkewX PerspX │ + * │ SkewY ScaleY TransY │ │ SkewY ScaleY PerspY │ + * │ PerspX PerspY PerspZ │ │ TransX TransY PerspZ │ + * └ ┘ └ ┘ + */ + + private static SKMatrix CreateElementsMatrix(float m11, float m12, float m21, float m22, float m31, float m32) + => new(m11, m12, m31, m21, m22, m32, 0, 0, 1); + + private static SKMatrix CreateTransformMatrix(PointF[] src, PointF[] dst) + { + if (src.Length < 3) throw new ArgumentException("must contain 3 points.", nameof(src)); + if (dst.Length < 3) throw new ArgumentException("must contain 3 points.", nameof(dst)); + + float den = (src[0].X - src[1].X) * (src[0].Y - src[2].Y) - (src[0].X - src[2].X) * (src[0].Y - src[1].Y); + if (den == 0) throw new InvalidOperationException("cannot create a valid transformation matrix for the given points."); + + float m11 = (dst[0].X * (src[1].Y - src[2].Y) + dst[1].X * (src[2].Y - src[0].Y) + dst[2].X * (src[0].Y - src[1].Y)) / den; + float m12 = (dst[0].Y * (src[1].Y - src[2].Y) + dst[1].Y * (src[2].Y - src[0].Y) + dst[2].Y * (src[0].Y - src[1].Y)) / den; + float dx = dst[0].X - m11 * src[0].X - m12 * src[0].Y; + + float m21 = (dst[0].X * (src[2].X - src[1].X) + dst[1].X * (src[0].X - src[2].X) + dst[2].X * (src[1].X - src[0].X)) / den; + float m22 = (dst[0].Y * (src[2].X - src[1].X) + dst[1].Y * (src[0].X - src[2].X) + dst[2].Y * (src[1].X - src[0].X)) / den; + float dy = dst[0].Y - m21 * src[0].X - m22 * src[0].Y; + + return CreateElementsMatrix(m11, m12, m21, m22, dx, dy); + } + + private static SKMatrix Multiply(SKMatrix a, SKMatrix b) + => SwapTrans(SKMatrix.Concat(SwapTrans(a), SwapTrans(b))); + + private static SKMatrix Invert(SKMatrix matrix) + => SwapTrans(matrix).TryInvert(out var invert) + ? SwapTrans(invert) + : throw new InvalidOperationException("matrix inversion failed."); + + private static SKMatrix SwapTrans(SKMatrix matrix) + => new( // swaps (TransX, TransY) with (Persp0, Persp1) + matrix.ScaleX, matrix.SkewX, matrix.Persp0, + matrix.SkewY, matrix.ScaleY, matrix.Persp1, + matrix.TransX, matrix.TransY, matrix.Persp2); + + private static SKMatrix Transpose(SKMatrix matrix) + => SwapTrans(new( // transposes the matrix + matrix.ScaleX, matrix.SkewY, matrix.Persp0, + matrix.SkewX, matrix.ScaleY, matrix.Persp1, + matrix.TransX, matrix.TransY, matrix.Persp2)); + + private static void TransformPoints(T[] points, SKMatrix matrix, Func newPoint, Func getPoint) + { + var transpose = Transpose(matrix); + for (int i = 0; i < points.Length; i++) + { + var point = transpose.MapPoint(getPoint(points[i])); + points[i] = newPoint(point); + } + } + + private void TransformVectors(T[] points, Func newPoint, Func getPoint) + { + var transformMatrix = new SKMatrix + { + ScaleX = m_matrix.ScaleX, + SkewX = m_matrix.SkewX, + TransX = 0, + SkewY = m_matrix.SkewY, + ScaleY = m_matrix.ScaleY, + TransY = 0, + Persp0 = m_matrix.Persp0, + Persp1 = m_matrix.Persp1, + Persp2 = 1 + }; + TransformPoints(points, transformMatrix, newPoint, getPoint); + } + + #endregion +} + \ No newline at end of file diff --git a/src/Common/Drawing2D/MatrixOrder.cs b/src/Common/Drawing2D/MatrixOrder.cs new file mode 100644 index 0000000..6d511b5 --- /dev/null +++ b/src/Common/Drawing2D/MatrixOrder.cs @@ -0,0 +1,17 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the order for matrix transform operations. +/// +public enum MatrixOrder +{ + /// + /// The new operation is applied after the old operation. + /// + Prepend = 0, + + /// + /// The new operation is applied before the old operation. + /// + Append = 1 +} diff --git a/src/Common/Point.cs b/src/Common/Point.cs index 05bd35c..75430fd 100644 --- a/src/Common/Point.cs +++ b/src/Common/Point.cs @@ -8,7 +8,7 @@ public struct Point : IEquatable { internal SKPoint m_point; - private Point(SKPoint point) + internal Point(SKPoint point) { m_point = point; } diff --git a/src/Common/PointF.cs b/src/Common/PointF.cs index b009356..77cbab9 100644 --- a/src/Common/PointF.cs +++ b/src/Common/PointF.cs @@ -8,7 +8,7 @@ public struct PointF : IEquatable { internal SKPoint m_point; - private PointF(SKPoint point) + internal PointF(SKPoint point) { m_point = point; } diff --git a/test/Common/Drawing2D/MatrixUnitTest.cs b/test/Common/Drawing2D/MatrixUnitTest.cs new file mode 100644 index 0000000..0aefc4c --- /dev/null +++ b/test/Common/Drawing2D/MatrixUnitTest.cs @@ -0,0 +1,208 @@ +using GeneXus.Drawing.Drawing2D; +using System; +using System.Numerics; + +namespace GeneXus.Drawing.Test.Drawing2D; + +internal class MatrixTests +{ + + private const float TOLERANCE = 0.001f; + + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_Default() + { + var matrix = new Matrix(); + Assert.Multiple(() => + { + Assert.That(matrix.IsIdentity, Is.True); + Assert.That(matrix.IsInvertible, Is.True); + Assert.That(matrix.Elements, Is.EquivalentTo(new float[] { 1, 0, 0, 1, 0, 0 })); + }); + } + + [Test] + public void Constructor_Elements() + { + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + Assert.Multiple(() => + { + Assert.That(matrix.IsIdentity, Is.False); + Assert.That(matrix.IsInvertible, Is.True); + Assert.That(matrix.Elements, Is.EqualTo(new float[] { 1, 2, 3, 4, 5, 6 })); + }); + } + + [Test] + public void Constructor_Matrix3x2() + { + var matrix3x2 = new Matrix3x2(1.0f, 0.0f, 0.0f, 1.0f, 50.0f, 50.0f); + + var matrix = new Matrix(matrix3x2); + Assert.That(matrix.MatrixElements, Is.EqualTo(matrix3x2)); + } + + [Test] + public void Constructor_RectAndPoints() + { + var rect = new RectangleF(0, 0, 100, 100); + var plgpts = new PointF[] { new(0, 0), new(100, 0), new(0, 100) }; + + var matrix = new Matrix(rect, plgpts); + Assert.That(matrix.Elements, Is.EqualTo(new float[] { 1, 0, 0, 1, 0, 0 }).Within(TOLERANCE)); + } + + [Test] + public void Operator_Equality() + { + var matrix1 = new Matrix(1, 2, 3, 4, 5, 6); + var matrix2 = new Matrix(1, 2, 3, 4, 5, 6); + Assert.Multiple(() => + { + Assert.That(matrix1 == matrix2, Is.True); + Assert.That(matrix1 != matrix2, Is.False); + }); + } + + [Test] + public void Method_Equals() + { + var matrix1 = new Matrix(1, 2, 3, 4, 5, 6); + + var matrix2 = new Matrix(1, 2, 3, 4, 5, 6); + Assert.That(matrix1.Equals(matrix2), Is.True); + + var matrix3 = new Matrix(6, 5, 4, 3, 2, 1); + Assert.That(matrix1.Equals(matrix3), Is.False); + } + + [Test] + public void Method_Clone() + { + var original = new Matrix(1, 2, 3, 4, 5, 6); + + var clone = original.Clone() as Matrix; + Assert.Multiple(() => + { + Assert.That(clone, Is.Not.Null); + Assert.That(clone, Is.EqualTo(original)); + Assert.That(ReferenceEquals(clone, original), Is.False); + Assert.That(clone.Elements, Is.EqualTo(original.Elements)); + }); + } + + [Test] + public void Method_Invert() + { + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + Assert.Multiple(() => + { + Assert.That(matrix.IsInvertible, Is.True); + + matrix.Invert(); + Assert.That(matrix.Elements, Is.EqualTo(new[] { -2, 1, 1.5f, -0.5f, 1, -2 }).Within(TOLERANCE)); + }); + } + + [Test] + public void Method_Multiply() + { + var matrix1 = new Matrix(1, 0, 0, 1, 10, 10); + var matrix2 = new Matrix(1, 0, 0, 1, 20, 20); + + matrix1.Multiply(matrix2); + Assert.That(matrix1.Elements, Is.EqualTo(new[] { 1, 0, 0, 1, 30, 30 })); + } + + [Test] + public void Method_Reset() + { + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + + matrix.Reset(); + Assert.That(matrix.IsIdentity, Is.True); + } + + [Test] + public void Method_Rotate() + { + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + + matrix.Rotate(45); + Assert.That(matrix.Elements, Is.EqualTo(new float[] { 2.828427f, 4.24264f, 1.414214f, 1.414214f, 5, 6 }).Within(TOLERANCE)); + } + + [Test] + public void Method_RotateAt() + { + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + + matrix.RotateAt(45, new PointF(10, 10)); + Assert.That(matrix.Elements, Is.EqualTo(new float[] { 2.828427f, 4.24264f, 1.414214f, 1.414214f, 2.573596f, 9.431463f }).Within(TOLERANCE)); + } + + [Test] + public void Method_Scale() + { + var matrix = new Matrix(); + + matrix.Scale(2, 2); + Assert.That(matrix.Elements, Is.Not.EqualTo(new[] { 2, 4, 9, 12, 5, 6 }).Within(TOLERANCE)); + } + + [Test] + public void Method_Shear() + { + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + + matrix.Shear(1, 2); + Assert.That(matrix.Elements, Is.EqualTo(new[] { 7, 10, 4, 6, 5, 6 }).Within(TOLERANCE)); + } + + [Test] + public void Method_Translate() + { + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + + matrix.Translate(50, 50); + Assert.Multiple(() => + { + Assert.That(matrix.OffsetX, Is.EqualTo(205)); + Assert.That(matrix.OffsetY, Is.EqualTo(306)); + }); + } + + + [Test] + public void Method_TransformPoints() + { + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + var points = new PointF[] { new(0, 0), new(10, 10) }; + + matrix.TransformPoints(points); + Assert.Multiple(() => + { + Assert.That(points[0], Is.EqualTo(new PointF(5, 6))); + Assert.That(points[1], Is.EqualTo(new PointF(45, 66))); + }); + } + + [Test] + public void Method_TransformVectors_ShouldTransformVectorsCorrectly() + { + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + var vectors = new Point[] { new(1, 0), new(0, 1) }; + + matrix.TransformVectors(vectors); + Assert.Multiple(() => + { + Assert.That(vectors[0], Is.EqualTo(new Point(1, 2))); + Assert.That(vectors[1], Is.EqualTo(new Point(3, 4))); + }); + } +} \ No newline at end of file From 8ea181bf4dc6af5647f4d3ac84e922c4fd570d0b Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 16:51:54 -0300 Subject: [PATCH 002/217] add StringFormat class and referenced enum/classes --- src/Common/CharacterRange.cs | 85 ++++++ src/Common/StringAlignment.cs | 26 ++ src/Common/StringDigitSubstitute.cs | 28 ++ src/Common/StringFormat.cs | 456 ++++++++++++++++++++++++++++ src/Common/StringFormatFlags.cs | 75 +++++ src/Common/StringTrimming.cs | 41 +++ src/Common/Text/HotkeyPrefix.cs | 22 ++ test/Common/StringFormatUnitTest.cs | 80 +++++ 8 files changed, 813 insertions(+) create mode 100644 src/Common/CharacterRange.cs create mode 100644 src/Common/StringAlignment.cs create mode 100644 src/Common/StringDigitSubstitute.cs create mode 100644 src/Common/StringFormat.cs create mode 100644 src/Common/StringFormatFlags.cs create mode 100644 src/Common/StringTrimming.cs create mode 100644 src/Common/Text/HotkeyPrefix.cs create mode 100644 test/Common/StringFormatUnitTest.cs diff --git a/src/Common/CharacterRange.cs b/src/Common/CharacterRange.cs new file mode 100644 index 0000000..4339f2f --- /dev/null +++ b/src/Common/CharacterRange.cs @@ -0,0 +1,85 @@ +using System; + +namespace GeneXus.Drawing; + +public struct CharacterRange : IEquatable +{ + public CharacterRange(int first, int length) + { + First = first; + Length = length; + } + + + #region Operators + + /// + /// Compares two objects. Gets a value indicating + /// whether the and values of the + /// two objects are equal. + /// + public static bool operator ==(CharacterRange cr1, CharacterRange cr2) => cr1.Equals(cr2); + + /// + /// Compares two objects. Gets a value indicating + /// whether the and values of the + /// two objects are not equal. + /// + public static bool operator !=(CharacterRange cr1, CharacterRange cr2) => !cr1.Equals(cr2); + + #endregion + + + #region IEqualitable + + /// + /// Indicates whether the current instance is equal to another instance of the same type. + /// + public readonly bool Equals(CharacterRange other) + => First == other.First && Length == other.Length; + + /// + /// Gets a value indicating whether this object is equivalent to the specified object. + /// + public override readonly bool Equals(object obj) + => obj is CharacterRange other && Equals(other); + + /// + /// Returns the hash code for this instance. + /// + public override readonly int GetHashCode() + => Combine(First, Length); + + #endregion + + + #region Properties + + /// + /// Gets or sets the position in the string of the first character of this . + /// + public int First { get; set; } + + /// + /// Gets or sets the number of positions in this . + /// + public int Length { get; set; } + + #endregion + + + #region Utilities + + private const uint PRIME1 = 2654435761U, PRIME2 = 2246822519U; + + private static int Combine(params object[] objects) + { + uint hash = PRIME1; + foreach (var obj in objects) + hash = hash * PRIME2 + (uint)obj.GetHashCode(); + return Convert.ToInt32(hash); + } + + #endregion +} + diff --git a/src/Common/StringAlignment.cs b/src/Common/StringAlignment.cs new file mode 100644 index 0000000..b062884 --- /dev/null +++ b/src/Common/StringAlignment.cs @@ -0,0 +1,26 @@ +namespace GeneXus.Drawing; + +/// +/// Specifies the alignment of a text string relative to its layout rectangle. +/// +public enum StringAlignment +{ + // left or top in English + /// + /// Specifies the text be aligned near the layout. In a left-to-right layout, the near position is left. In a + /// right-to-left layout, the near position is right. + /// + Near = 0, + + /// + /// Specifies that text is aligned in the center of the layout rectangle. + /// + Center = 1, + + // right or bottom in English + /// + /// Specifies that text is aligned far from the origin position of the layout rectangle. In a left-to-right + /// layout, the far position is right. In a right-to-left layout, the far position is left. + /// + Far = 2 +} diff --git a/src/Common/StringDigitSubstitute.cs b/src/Common/StringDigitSubstitute.cs new file mode 100644 index 0000000..972d867 --- /dev/null +++ b/src/Common/StringDigitSubstitute.cs @@ -0,0 +1,28 @@ +namespace GeneXus.Drawing; + +/// +/// Specifies style information applied to String Digit Substitute. +/// +public enum StringDigitSubstitute +{ + /// + /// Specifies a user-defined substitution scheme. + /// + User = 0, + + /// + /// Specifies to disable substitutions. + /// + None = 1, + + /// + /// Specifies substitution digits that correspond with the official national language of the user's locale. + /// + National = 2, + + /// + /// Specifies substitution digits that correspond with the user's native script or language, which may be + /// different from the official national language of the user's locale. + /// + Traditional = 3 +} diff --git a/src/Common/StringFormat.cs b/src/Common/StringFormat.cs new file mode 100644 index 0000000..db2641c --- /dev/null +++ b/src/Common/StringFormat.cs @@ -0,0 +1,456 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using GeneXus.Drawing.Text; +using SkiaSharp; + +namespace GeneXus.Drawing; + +public sealed class StringFormat : ICloneable, IDisposable +{ + private float TabOffset = 0; + private float[] TabStops = Array.Empty(); + private CharacterRange[] Ranges = Array.Empty(); + + /// + /// Initializes a new instance of the class. + /// + public StringFormat() + : this(0, 0) { } + + /// + /// Initializes a new instance of the class with the specified . + /// + public StringFormat(StringFormatFlags options) + : this(options, 0) { } + + /// + /// Initializes a new instance of the class with the specified + /// and language. + /// + public StringFormat(StringFormatFlags options, int language) + { + FormatFlags = options; + DigitSubstitutionLanguage = language; + } + + /// + /// Initializes a new instance of the class from the specified + /// existing . + /// + public StringFormat(StringFormat format) + => format.MemberwiseClone(); + + /// + /// Cleans up resources for this . + /// + ~StringFormat() => Dispose(false); + + /// + /// Converts this to a human-readable string. + /// + public override string ToString() => $"[StringFormat, FormatFlags={FormatFlags}]"; + + + #region IDisposable + + /// + /// Cleans up resources for this . + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + private void Dispose(bool disposing) { } + + #endregion + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public object Clone() => new StringFormat(this); + + #endregion + + + #region Properties + + /// + /// Specifies text alignment information. + /// + public StringAlignment Alignment { get; set; } = StringAlignment.Near; + + /// + /// Gets the for this . + /// + public StringDigitSubstitute DigitSubstitutionMethod { get; private set; } = StringDigitSubstitute.None; + + /// + /// Gets the language of for this . + /// + public int DigitSubstitutionLanguage { get; private set; } + + /// + /// Gets or sets a that contains formatting information. + /// + public StringFormatFlags FormatFlags { get; set; } = 0; + + /// + /// Gets a generic default . + /// + public static StringFormat GenericDefault => new(); + + /// + /// Gets a generic typographic . + /// + public static StringFormat GenericTypographic => new() + { + FormatFlags = StringFormatFlags.FitBlackBox | StringFormatFlags.LineLimit, + Trimming = StringTrimming.None + }; + + /// + /// Gets or sets the for this . + /// + public HotkeyPrefix HotkeyPrefix { get; set; } = HotkeyPrefix.None; + + /// + /// Gets or sets the line alignment. + /// + public StringAlignment LineAlignment { get; set; } = StringAlignment.Near; + + /// + /// Gets or sets the for this . + /// + public StringTrimming Trimming { get; set; } = StringTrimming.None; + + #endregion + + + #region Methods + + /// + /// Gets the ranges count for this object. + /// + internal int GetMeasurableCharacterRangeCount() + => Ranges.Length; + + /// + /// Gets the tab stops for this object. + /// + public float[] GetTabStops(out float firstTabOffset) + { + firstTabOffset = TabOffset; + return TabStops; + } + + /// + /// Specifies the language and method to be used when local digits are substituted for western digits. + /// + public void SetDigitSubstitution(int language, StringDigitSubstitute substitute) + { + DigitSubstitutionLanguage = language; + DigitSubstitutionMethod = substitute; + } + + /// + /// Sets the measure of characters to the specified range. + /// + public void SetMeasurableCharacterRanges(CharacterRange[] ranges) + => Ranges = ranges; + + /// + /// Sets tab stops for this . + /// + public void SetTabStops(float firstTabOffset, float[] tabStops) + { + TabOffset = firstTabOffset; + TabStops = tabStops; + } + + #endregion + + + #region Utilties + + internal static readonly string[] BREAKLINES = new string[] + { + "\r\n", + "\n", + "\u2028", // unicode's line separator + "\u2029", // unicode's paragraph separator + "\u0085", // unicode's next line + }; + + internal static readonly char[] CONTROL = Enumerable.Range(0x00, 0xFFFF) + .Select(Convert.ToChar) + .Where(char.IsControl) + .ToArray(); + + internal static readonly Dictionary CONTROL_CHARACTERS = new Dictionary + { + { '\u0000', '␀' }, // Null + { '\u0001', '␁' }, // Start of Heading + { '\u0002', '␂' }, // Start of Text + { '\u0003', '␃' }, // End of Text + { '\u0004', '␄' }, // End of Transmission + { '\u0005', '␅' }, // Enquiry + { '\u0006', '␆' }, // Acknowledge + { '\u0007', '␇' }, // Bell + { '\u0008', '␈' }, // Backspace + { '\u0009', '␉' }, // Horizontal Tab + { '\u000A', '␊' }, // Line Feed + { '\u000B', '␋' }, // Vertical Tab + { '\u000C', '␌' }, // Form Feed + { '\u000D', '␍' }, // Carriage Return + { '\u000E', '␎' }, // Shift Out + { '\u000F', '␏' }, // Shift In + { '\u0010', '␐' }, // Data Link Escape + { '\u0011', '␑' }, // Device Control 1 + { '\u0012', '␒' }, // Device Control 2 + { '\u0013', '␓' }, // Device Control 3 + { '\u0014', '␔' }, // Device Control 4 + { '\u0015', '␕' }, // Negative Acknowledge + { '\u0016', '␖' }, // Synchronous Idle + { '\u0017', '␗' }, // End of Transmission Block + { '\u0018', '␘' }, // Cancel + { '\u0019', '␙' }, // End of Medium + { '\u001A', '␚' }, // Substitute + { '\u001B', '␛' }, // Escape + { '\u001C', '␜' }, // File Separator + { '\u001D', '␝' }, // Group Separator + { '\u001E', '␞' }, // Record Separator + { '\u001F', '␟' }, // Unit Separator + { '\u007F', '␡' }, // Delete + { '\u0085', '␤' }, // Next Line + { '\u00A0', '␣' }, // Non-Breaking Space + { '\u1680', ' ' }, // Ogham Space Mark + { '\u2000', ' ' }, // En Quad + { '\u2001', ' ' }, // Em Quad + { '\u2002', ' ' }, // En Space + { '\u2003', ' ' }, // Em Space + { '\u2004', ' ' }, // Three-Per-Em Space + { '\u2005', ' ' }, // Four-Per-Em Space + { '\u2006', ' ' }, // Six-Per-Em Space + { '\u2007', ' ' }, // Figure Space + { '\u2008', ' ' }, // Punctuation Space + { '\u2009', ' ' }, // Thin Space + { '\u200A', ' ' }, // Hair Space + { '\u200B', '​' }, // Zero Width Space + { '\u200C', '‌' }, // Zero Width Non-Joiner + { '\u200D', '‍' }, // Zero Width Joiner + { '\u200E', '‎' }, // Left-to-Right Mark + { '\u200F', '‏' }, // Right-to-Left Mark + { '\u2028', '␤' }, // Line Separator + { '\u2029', '¶' }, // Paragraph Separator + { '\u202A', '‪' }, // Left-to-Right Embedding + { '\u202B', '‫' }, // Right-to-Left Embedding + { '\u202C', '‬' }, // Pop Directional Formatting + { '\u202D', '‭' }, // Left-to-Right Override + { '\u202E', '‮' }, // Right-to-Left Override + { '\u2060', '⁠' }, // Word Joiner + { '\u2061', '⁡' }, // Function Application + { '\u2062', '⁢' }, // Invisible Times + { '\u2063', '⁣' }, // Invisible Separator + { '\u2064', '⁤' } // Invisible Plus + }; + + #endregion + + + #region Helpers + + internal string ApplyDirection(string text) + => FormatFlags.HasFlag(StringFormatFlags.DirectionVertical) + ? string.Join("\n", text.Split(BREAKLINES, StringSplitOptions.None).Reverse()) + : text; + + + internal string ApplyHotkey(string text, out int[] indexes) + { + var hkIndexes = new List(); + + var sb = new StringBuilder(); + for (int i = 0; i < text.Length; i++) + { + var c = text[i]; + if (c == '&') + { + if (i < text.Length - 1 && text[i + 1] == '&') + { + i++; // skip next '&' + } + else + { + if (HotkeyPrefix == HotkeyPrefix.Show) + hkIndexes.Add(sb.Length); + if (HotkeyPrefix != HotkeyPrefix.None) + continue; + } + } + sb.Append(c); + } + indexes = hkIndexes.ToArray(); + return sb.ToString(); + } + + + internal string ApplyTabStops(string text) + { + var tabStops = GetTabStops(out var tabOffset); + if (tabStops.Length > 0) + { + var fill = string.Empty.PadLeft((int)tabOffset, ' '); + + var lines = text.Split('\n'); + for (int i = 0; i < lines.Length; i++) + { + var columns = lines[i].Split('\t'); + for (int j = 0; j < columns.Length; j++) + { + int pad = j < tabStops.Length ? (int)tabStops[j] : columns[j].Length; + columns[j] = columns[j].PadLeft(pad, ' '); + } + lines[i] = fill + string.Join("", columns); + } + text = string.Join("", lines); + } + return text; + } + + + internal string ApplyControlEscape(string text) + { + var sb = new StringBuilder(text); + if (FormatFlags.HasFlag(StringFormatFlags.DisplayFormatControl)) + { + var controlChars = CONTROL_CHARACTERS; + foreach (char chr in CONTROL) + if (!controlChars.ContainsKey(chr)) + controlChars.Add(chr, '�'); + + foreach (var kvp in controlChars) + sb.Replace(kvp.Key, kvp.Value); + } + return sb.ToString(); + } + + + internal string ApplyWrapping(string text, SKRect boundBox, Func measureText) + { + if (FormatFlags.HasFlag(StringFormatFlags.NoWrap)) + return text; + + float accWidth = 0; + + var sb = new StringBuilder(); + foreach (var line in text.Split(BREAKLINES, StringSplitOptions.None)) + { + foreach (var word in line.Split(' ')) + { + var curWidth = measureText(word); + if ((accWidth += curWidth) > boundBox.Width) + { + sb.Append('\n').Append(' '); + accWidth = curWidth; + } + sb.Append(word).Append(' '); + } + sb.Length--; // remove last space + } + return sb.ToString(); + } + + + private string ApplyTrimming(IEnumerable tokens, string endToken, SKRect boundBox, Func measureText) + { + float accWidth = 0; + float endLen = measureText(endToken); + + var sb = new StringBuilder(); + var tokenList = tokens.ToArray(); + for (int i = 0; i < tokenList.Length; i++) + { + string token = tokenList[i]; + + float curWidth = measureText(token); + if (token == "&") + { + if (i < tokenList.Length - 1 && tokenList[i + 1] == "&") + { + i++; // skip next '&' + } + else if (HotkeyPrefix != HotkeyPrefix.None) + { + curWidth = 0; + } + } + + if ((accWidth += curWidth) + endLen > boundBox.Width) + { + if (FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft)) + sb.Insert(0, endToken); + else + sb.Append(endToken); + break; + } + sb.Append(token); + } + + var trim = (string s) => FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft) ? s.TrimStart() : s.TrimEnd(); + return trim(sb.ToString()); + } + + + internal string ApplyTrimming(string text, SKRect boundBox, float lineHeight, Func measureText) + { + var lines = text.Split(BREAKLINES, StringSplitOptions.None); + if (FormatFlags.HasFlag(StringFormatFlags.LineLimit)) + lines = lines.Take((int)(boundBox.Height / lineHeight)).ToArray(); + + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + + if (!FormatFlags.HasFlag(StringFormatFlags.MeasureTrailingSpaces)) + line = FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft) ? line.TrimStart() : line.TrimEnd(); + + line = Trimming switch + { + StringTrimming.None + => line, + + StringTrimming.Character + => ApplyTrimming(line.Select(chr => chr.ToString()), "", boundBox, measureText), + + StringTrimming.Word + => ApplyTrimming(line.Split(' '), "", boundBox, measureText), + + StringTrimming.EllipsisCharacter + => ApplyTrimming(line.Select(chr => chr.ToString()), "…", boundBox, measureText), + + StringTrimming.EllipsisWord + => ApplyTrimming(line.Split(' '), "…", boundBox, measureText), + + StringTrimming.EllipsisPath + => ApplyTrimming(line.Split(Path.PathSeparator), "…", boundBox, measureText), + + _ => throw new NotImplementedException($"trimming value {Trimming}") + }; + + lines[i] = line; + } + + return string.Join("\n", lines).Trim(); + } + + #endregion +} diff --git a/src/Common/StringFormatFlags.cs b/src/Common/StringFormatFlags.cs new file mode 100644 index 0000000..ee2c93e --- /dev/null +++ b/src/Common/StringFormatFlags.cs @@ -0,0 +1,75 @@ +using System; + +namespace GeneXus.Drawing; + +/// +/// Specifies the display and layout information for text strings. +/// +[Flags] +public enum StringFormatFlags +{ + /// + /// Specifies that text is right to left. + /// + DirectionRightToLeft = 0x00000001, + + /// + /// Specifies that text is vertical. + /// + DirectionVertical = 0x00000002, + + /// + /// Specifies that no part of any glyph overhangs the bounding rectangle. By default some glyphs + /// overhang the rectangle slightly where necessary to appear at the edge visually. For example + /// when an italic lower case letter f in a font such as Garamond is aligned at the far left + /// of a rectangle, the lower part of the f will reach slightly further left than the left edge + /// of the rectangle. Setting this flag will ensure no painting outside the rectangle but will + /// cause the aligned edges of adjacent lines of text to appear uneven. + /// + FitBlackBox = 0x00000004, + + /// + /// Causes control characters such as the left-to-right mark to be shown in the output with a representative glyph. + /// + DisplayFormatControl = 0x00000020, + + /// + /// Disables fallback to alternate fonts for characters not supported in the requested font. Any missing characters are + /// displayed with the fonts missing glyph, usually an open square. + /// + NoFontFallback = 0x00000400, + + /// + /// Specifies that the space at the end of each line is included in a string measurement. + /// + MeasureTrailingSpaces = 0x00000800, + + /// + /// Specifies that the wrapping of text to the next line is disabled. NoWrap is implied when a point of origin + /// is used instead of a layout rectangle. When drawing text within a rectangle, by default, text is broken at + /// the last word boundary that is inside the rectangle's boundary and wrapped to the next line. + /// + NoWrap = 0x00001000, + + /// + /// Specifies that only entire lines are laid out in the layout rectangle. By default, layout + /// continues until the end of the text or until no more lines are visible as a result of clipping, + /// whichever comes first. The default settings allow the last line to be partially obscured by a + /// layout rectangle that is not a whole multiple of the line height. + /// To ensure that only whole lines are seen, set this flag and be careful to provide a layout + /// rectangle at least as tall as the height of one line. + /// + LineLimit = 0x00002000, + + /// + /// Specifies that characters overhanging the layout rectangle and text extending outside the layout + /// rectangle are allowed to show. By default, all overhanging characters and text that extends outside + /// the layout rectangle are clipped. Any trailing spaces (spaces that are at the end of a line) that + /// extend outside the layout rectangle are clipped. Therefore, the setting of this flag will have an + /// effect on a string measurement if trailing spaces are being included in the measurement. + /// If clipping is enabled, trailing spaces that extend outside the layout rectangle are not included + /// in the measurement. If clipping is disabled, all trailing spaces are included in the measurement, + /// regardless of whether they are outside the layout rectangle. + /// + NoClip = 0x00004000 +} diff --git a/src/Common/StringTrimming.cs b/src/Common/StringTrimming.cs new file mode 100644 index 0000000..a7fc3e1 --- /dev/null +++ b/src/Common/StringTrimming.cs @@ -0,0 +1,41 @@ +namespace GeneXus.Drawing; + +/// +/// Specifies how to trim characters from a string that does not completely fit into a layout shape. +/// +public enum StringTrimming +{ + /// + /// Specifies no trimming. + /// + None = 0, + + /// + /// Specifies that the string is broken at the boundary of the last character + /// that is inside the layout rectangle. This is the default. + /// + Character = 1, + + /// + /// Specifies that the string is broken at the boundary of the last word that is inside the layout rectangle. + /// + Word = 2, + + /// + /// Specifies that the string is broken at the boundary of the last character that is inside + /// the layout rectangle and an ellipsis (...) is inserted after the character. + /// + EllipsisCharacter = 3, + + /// + /// Specifies that the string is broken at the boundary of the last word that is inside the + /// layout rectangle and an ellipsis (...) is inserted after the word. + /// + EllipsisWord = 4, + + /// + /// Specifies that the center is removed from the string and replaced by an ellipsis. + /// The algorithm keeps as much of the last portion of the string as possible. + /// + EllipsisPath = 5 +} diff --git a/src/Common/Text/HotkeyPrefix.cs b/src/Common/Text/HotkeyPrefix.cs new file mode 100644 index 0000000..dd3a28a --- /dev/null +++ b/src/Common/Text/HotkeyPrefix.cs @@ -0,0 +1,22 @@ +namespace GeneXus.Drawing.Text; + +/// +/// Specifies the type of display for hotkey prefixes for text. +/// +public enum HotkeyPrefix +{ + /// + /// No hotkey prefix. + /// + None = 0, + + /// + /// Display the hotkey prefix. + /// + Show = 1, + + /// + /// Do not display the hotkey prefix. + /// + Hide = 2 +} diff --git a/test/Common/StringFormatUnitTest.cs b/test/Common/StringFormatUnitTest.cs new file mode 100644 index 0000000..406786d --- /dev/null +++ b/test/Common/StringFormatUnitTest.cs @@ -0,0 +1,80 @@ +namespace GeneXus.Drawing.Test; + +internal class StringFormatUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_Default() + { + var format = new StringFormat(); + Assert.Multiple(() => + { + Assert.That(format.FormatFlags, Is.EqualTo((StringFormatFlags)0)); + Assert.That(format.Alignment, Is.EqualTo(StringAlignment.Near)); + Assert.That(format.LineAlignment, Is.EqualTo(StringAlignment.Near)); + Assert.That(format.Trimming, Is.EqualTo(StringTrimming.None)); + Assert.That(format.DigitSubstitutionMethod, Is.EqualTo(StringDigitSubstitute.None)); + Assert.That(format.DigitSubstitutionLanguage, Is.EqualTo(0)); + }); + } + + [Test] + public void Constructor_FormatFlags() + { + var format = new StringFormat(StringFormatFlags.DirectionVertical | StringFormatFlags.DirectionRightToLeft); + Assert.Multiple(() => + { + Assert.That(format.FormatFlags.HasFlag(StringFormatFlags.DirectionVertical)); + Assert.That(format.FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft)); + Assert.That(!format.FormatFlags.HasFlag(StringFormatFlags.NoClip)); + Assert.That(format.DigitSubstitutionLanguage, Is.EqualTo(0)); + }); + } + + [Test] + public void Constructor_FormatFlagsAndLanguage() + { + var format = new StringFormat(StringFormatFlags.DirectionVertical | StringFormatFlags.DirectionRightToLeft, 1033); + Assert.Multiple(() => + { + Assert.That(format.FormatFlags.HasFlag(StringFormatFlags.DirectionVertical)); + Assert.That(format.FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft)); + Assert.That(!format.FormatFlags.HasFlag(StringFormatFlags.NoClip)); + Assert.That(format.DigitSubstitutionLanguage, Is.EqualTo(1033)); + }); + } + + [Test] + public void Method_GetSetDigitSubstitution() + { + var format = new StringFormat(); + + format.SetDigitSubstitution(1033, StringDigitSubstitute.Traditional); + Assert.Multiple(() => + { + Assert.That(format.DigitSubstitutionLanguage, Is.EqualTo(1033)); + Assert.That(format.DigitSubstitutionMethod, Is.EqualTo(StringDigitSubstitute.Traditional)); + }); + } + + [Test] + public void Method_GetSetTabStops() + { + var format = new StringFormat(); + + float expTabOffset = 2.0f; + float[] expTabStops = { 4.0f, 8.0f, 12.0f }; + format.SetTabStops(expTabOffset, expTabStops); + + float[] tabStops = format.GetTabStops(out float tabOffset); + Assert.Multiple(() => + { + Assert.That(tabOffset, Is.EqualTo(expTabOffset)); + Assert.That(tabStops, Is.EqualTo(expTabStops)); + }); + } +} \ No newline at end of file From a33518ba539bd792812d21139e430c94828812e6 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 17:04:21 -0300 Subject: [PATCH 003/217] add GraphicsPath and its dependencies (except for Pen, Brush and Graphics classes) --- src/Common/Drawing2D/FillMode.cs | 17 + src/Common/Drawing2D/GraphicsPath.cs | 896 ++++++++++++++++++++++++++ src/Common/Drawing2D/PathData.cs | 25 + src/Common/Drawing2D/PathPointType.cs | 47 ++ src/Common/Drawing2D/WarpMode.cs | 17 + 5 files changed, 1002 insertions(+) create mode 100644 src/Common/Drawing2D/FillMode.cs create mode 100644 src/Common/Drawing2D/GraphicsPath.cs create mode 100644 src/Common/Drawing2D/PathData.cs create mode 100644 src/Common/Drawing2D/PathPointType.cs create mode 100644 src/Common/Drawing2D/WarpMode.cs diff --git a/src/Common/Drawing2D/FillMode.cs b/src/Common/Drawing2D/FillMode.cs new file mode 100644 index 0000000..eb44ae0 --- /dev/null +++ b/src/Common/Drawing2D/FillMode.cs @@ -0,0 +1,17 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies how the interior of a closed path is filled. +/// +public enum FillMode +{ + /// + /// Specifies the alternate fill mode. + /// + Alternate = 0, // Odd-even fill rule + + /// + /// Specifies the winding fill mode. + /// + Winding = 1, // Non-zero winding fill rule +} diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs new file mode 100644 index 0000000..dcb956b --- /dev/null +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -0,0 +1,896 @@ + +using System; +using System.Linq; +using SkiaSharp; + +namespace GeneXus.Drawing.Drawing2D; + +public sealed class GraphicsPath : ICloneable, IDisposable +{ + internal readonly SKPath m_path; + + private GraphicsPath(SKPath path) + { + m_path = path; + } + + /// + /// Initializes a new instance of the class with + /// a value of Alternate. + /// + public GraphicsPath() + : this(FillMode.Alternate) { } + + /// + /// Initializes a new instance of the class with + /// the enumeration. + /// + public GraphicsPath(FillMode mode) + : this(Array.Empty(), Array.Empty(), mode) { } + + /// + /// Initializes a new instance of the class with + /// the specified and arrays and with the + /// specified enumeration element. + /// + public GraphicsPath(PointF[] points, byte[] types, FillMode mode = FillMode.Alternate) + : this(CreatePath(points, types, mode)) { } + + /// + /// Initializes a new instance of the class with + /// the specified and arrays and with the + /// specified enumeration element. + /// + public GraphicsPath(Point[] points, byte[] types, FillMode mode = FillMode.Alternate) + : this(Array.ConvertAll(points, point => new PointF(point.m_point)), types, mode) { } + + /// + /// Cleans up resources for this . + /// + ~GraphicsPath() => Dispose(false); + + + #region IDisposable + + /// + /// Cleans up resources for this . + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + private void Dispose(bool disposing) => m_path.Dispose(); + + #endregion + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public object Clone() + => new GraphicsPath(m_path); + + #endregion + + + #region Operators + + /// + /// Creates a with the coordinates of the specified . + /// + public static explicit operator SKPath(GraphicsPath path) => path.m_path; + + #endregion + + + #region Properties + + /// + /// Gets or sets a enumeration that determines how the + /// interiors of shapes in this are filled. + /// + public FillMode FillMode + { + get => m_path.FillType switch + { + SKPathFillType.EvenOdd => FillMode.Alternate, + SKPathFillType.Winding => FillMode.Winding, + _ => throw new NotImplementedException($"value {m_path.FillType}") + }; + set => m_path.FillType = value switch + { + FillMode.Alternate => SKPathFillType.EvenOdd, + FillMode.Winding => SKPathFillType.Winding, + _ => throw new NotImplementedException($"value {value}") + }; + } + + /// + /// Gets a that encapsulates arrays of points and types + /// for this . + /// + public PathData PathData => new() + { + Points = PathPoints, + Types = PathTypes + }; + + /// + /// Gets the points in the path. + /// + public PointF[] PathPoints => Array.ConvertAll(m_path.Points, point => new PointF(point)); + + /// + /// Gets the types of the corresponding points in the array. + /// + public byte[] PathTypes + { + get + { + using var iterator = m_path.CreateIterator(false); + byte[] types = new byte[m_path.PointCount]; + + int index = 0; + var points = new SKPoint[4]; + SKPathVerb verb; + while ((verb = iterator.Next(points)) != SKPathVerb.Done) + { + types[index] = (byte)(types[index] | verb switch + { + SKPathVerb.Move => (byte)PathPointType.Start, + SKPathVerb.Line => (byte)PathPointType.Line, + SKPathVerb.Cubic => (byte)PathPointType.Bezier, + SKPathVerb.Close => (byte)PathPointType.CloseSubpath, + _ => throw new NotImplementedException($"verb {verb}") + }); + index = Math.Min(++index, types.Length - 1); + } + + return types; + } + } + + /// + /// Gets the number of elements in the or the array. + /// + public int PointCount => m_path.PointCount; + + #endregion + + + #region Methods + + /// + /// Appends an elliptical arc to the current figure bounded + /// by a rectangle defined by position and size. + /// + public void AddArc(int x, int y, int width, int height, float startAngle, float sweepAngle) + => AddArc(new Rectangle(x, y, width, height), startAngle, sweepAngle); + + /// + /// Appends an elliptical arc to the current figure bounded + /// by a structure. + /// + public void AddArc(Rectangle rect, float startAngle, float sweepAngle) + => AddArc(rect.m_rect, startAngle, sweepAngle); + + /// + /// Appends an elliptical arc to the current figure bounded + /// by a rectangle defined by position and size. + /// + public void AddArc(float x, float y, float width, float height, float startAngle, float sweepAngle) + => AddArc(new RectangleF(x, y, width, height), startAngle, sweepAngle); + + /// + /// Appends an elliptical arc to the current figure bounded + /// by a structure. + /// + public void AddArc(RectangleF rect, float startAngle, float sweepAngle) + => AddArc(rect.m_rect, startAngle, sweepAngle); + + /// + /// Adds a cubic Bézier curve to the current figure defined + /// by 4 points' coordinates. + /// + public void AddBezier(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) + => AddBezier(new Point(x1, y1), new Point(x2, y2), new Point(x3, y3), new Point(x4, y4)); + + /// + /// Adds a cubic Bézier curve to the current figure defined + /// by 4 structures. + /// + public void AddBezier(Point pt1, Point pt2, Point pt3, Point pt4) + => AddBezier(pt1.m_point, pt2.m_point, pt3.m_point, pt4.m_point); + + /// + /// Adds a cubic Bézier curve to the current figure defined + /// by 4 points' coordinates. + /// + public void AddBezier(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) + => AddBezier(new PointF(x1, y1), new PointF(x2, y2), new PointF(x3, y3), new PointF(x4, y4)); + + /// + /// Adds a cubic Bézier curve to the current figure defined + /// by 4 structures. + /// + public void AddBezier(PointF pt1, PointF pt2, PointF pt3, PointF pt4) + => AddBezier(pt1.m_point, pt2.m_point, pt3.m_point, pt4.m_point); + + /// + /// Adds a sequence of connected cubic Bézier curves defined by an + /// array of structure to the current figure. + /// + public void AddBeziers(params PointF[] points) + => AddBeziers(Array.ConvertAll(points, point => point.m_point)); + + /// + /// Adds a sequence of connected cubic Bézier curves defined by an + /// array of structure to the current figure. + /// + public void AddBeziers(params Point[] points) + => AddBeziers(Array.ConvertAll(points, point => point.m_point)); + + /// + /// Adds a closed curve to this path defined by a sequence + /// of structure. A cardinal spline + /// curve is used because the curve travels through each + /// of the points in the array. + /// + public void AddClosedCurve(params PointF[] points) + => AddClosedCurve(points); + + /// + /// Adds a closed curve to this path defined by an array + /// of structure. + /// + public void AddClosedCurve(PointF[] points, float tension = 0.5f) + => AddCurve(Array.ConvertAll(points, point => point.m_point), tension, true); + + /// + /// Adds a closed curve to this path defined by a sequence + /// of structure. A cardinal spline + /// curve is used because the curve travels through each + /// of the points in the array. + /// + public void AddClosedCurve(params Point[] points) + => AddClosedCurve(points); + + /// + /// Adds a closed curve to this path defined by an array + /// of structure. + /// + public void AddClosedCurve(Point[] points, float tension = 0.5f) + => AddCurve(Array.ConvertAll(points, point => point.m_point), tension, true); + + /// + /// Adds a spline curve to the current figure defined + /// by a sequence of structures. A + /// cardinal spline curve is used because the curve + /// travels through each of the points in the array. + /// + public void AddCurve(params PointF[] points) + => AddCurve(points); + + /// + /// Adds a spline curve to the current figure defined + /// by an array of structure. + /// + public void AddCurve(PointF[] points, float tension = 0.5f) + => AddCurve(points, 0, points.Length - 1, tension); + + /// + /// Adds a spline curve to the current figure defined + /// by an array of structure but taking + /// a certain number of segments starting from an offset. + /// + public void AddCurve(PointF[] points, int offset, int numberOfSegments, float tension = 0.5f) + => AddCurve(points.Skip(offset).Take(numberOfSegments + 1).Select(p => p.m_point).ToArray(), tension, false); + + /// + /// Adds a spline curve to the current figure defined + /// by a sequence of structures. A + /// cardinal spline curve is used because the curve + /// travels through each of the points in the array. + /// + public void AddCurve(params Point[] points) + => AddCurve(points); + + /// + /// Adds a spline curve to the current figure defined + /// by an array of structure. + /// + public void AddCurve(Point[] points, float tension = 0.5f) + => AddCurve(points, 0, points.Length - 1, tension); + + /// + /// Adds a spline curve to the current figure defined + /// by an array of structure but taking + /// a certain number of segments starting from an offset. + /// + public void AddCurve(Point[] points, int offset, int numberOfSegments, float tension = 0.5f) + => AddCurve(points.Skip(offset).Take(numberOfSegments + 1).Select(p => p.m_point).ToArray(), tension, false); + + /// + /// Adds an ellipse to the current path bounded + /// by a rectangle defined by position and size. + /// + public void AddEllipse(float x, float y, float width, float height) + => AddEllipse(new RectangleF(x, y, width, height)); + + /// + /// Adds an ellipse to the current path bounded by + /// a structure. + /// + public void AddEllipse(RectangleF rect) + => AddEllipse(rect.m_rect); + + /// + /// Adds an ellipse to the current path bounded + /// by a rectangle defined by position and size. + /// + public void AddEllipse(int x, int y, int width, int height) + => AddEllipse(new Rectangle(x, y, width, height)); + + /// + /// Adds an ellipse to the current path bounded by + /// a structure. + /// + public void AddEllipse(Rectangle rect) + => AddEllipse(rect.m_rect); + + /// + /// Appends a line segment to the current figure defined by + /// the coordinates of the starting and ending points. + /// + public void AddLine(float x1, float y1, float x2, float y2) + => AddLine(new PointF(x1, y1), new PointF(x2, y2)); + + /// + /// Appends a line segment to the current figure defined by + /// the starting and ending structures. + /// + public void AddLine(PointF pt1, PointF pt2) + => AddLine(pt1.m_point, pt2.m_point); + + /// + /// Appends a line segment to the current figure defined by + /// the coordinates of the starting and ending points. + /// + public void AddLine(int x1, int y1, int x2, int y2) + => AddLine(new Point(x1, y1), new Point(x2, y2)); + + /// + /// Appends a line segment to the current figure defined by + /// the starting and ending structures. + /// + public void AddLine(Point pt1, Point pt2) + => AddLine(pt1.m_point, pt2.m_point); + + /// + /// Appends a series of connected line segments to the end + /// of the current figure defined by a sequence of structures. + /// + public void AddLines(params PointF[] points) + => Array.ForEach(Enumerable.Range(0, points.Length - 2).ToArray(), i => AddLine(points[i], points[i + 1])); + + /// + /// Appends a series of connected line segments to the end + /// of the current figure defined by a sequence of structures. + /// + public void AddLines(params Point[] points) + => Array.ForEach(Enumerable.Range(0, points.Length - 2).ToArray(), i => AddLine(points[i], points[i + 1])); + + /// + /// Appends the specified to this path. + /// + public void AddPath(GraphicsPath path, bool connect) + => m_path.AddPath(path.m_path, connect ? SKPathAddMode.Append : SKPathAddMode.Extend); + + /// + /// Adds the outline of a pie shape to this path bounded + /// by a rectangle defined by position and size. + /// + public void AddPie(float x, float y, float width, float height, float startAngle, float sweepAngle) + => AddPie(new RectangleF(x, y, width, height), startAngle, sweepAngle); + + /// + /// Adds the outline of a pie shape to this path bounded + /// by a structure. + /// + public void AddPie(RectangleF rect, float startAngle, float sweepAngle) + => AddPie(rect.m_rect, startAngle, sweepAngle); + + /// + /// Adds the outline of a pie shape to this path bounded + /// by a rectangle defined by position and size. + /// + public void AddPie(int x, int y, int width, int height, float startAngle, float sweepAngle) + => AddPie(new Rectangle(x, y, width, height), startAngle, sweepAngle); + + /// + /// Adds the outline of a pie shape to this path bounded + /// by a structure. + /// + public void AddPie(Rectangle rect, float startAngle, float sweepAngle) + => AddPie(rect.m_rect, startAngle, sweepAngle); + + /// + /// Adds a polygon to this path defined by an array of structures. + /// + public void AddPolygon(PointF[] points) + => AddPolygon(Array.ConvertAll(points, point => point.m_point)); + + /// + /// Adds a polygon to this path defined by an array of structures. + /// + public void AddPolygon(Point[] points) + => AddPolygon(Array.ConvertAll(points, point => point.m_point)); + + /// + /// Adds a structure to this path. + /// + public void AddRectangle(RectangleF rect) + => AddRectangle(rect.m_rect); + + /// + /// Adds a structure to this path. + /// + public void AddRectangle(Rectangle rect) + => AddRectangle(rect.m_rect); + + /// + /// Adds a series of structures to this path. + /// + public void AddRectangles(params RectangleF[] rects) + => Array.ForEach(rects, AddRectangle); + + /// + /// Adds a series of structures to this path. + /// + public void AddRectangles(params Rectangle[] rects) + => Array.ForEach(rects, AddRectangle); + + /// + /// Adds a text string to this path bounded by structure. + /// + public void AddString(string text, FontFamily family, int style, float emSize, RectangleF layout, StringFormat format) + => AddString(text, family, style, emSize, layout.m_rect, format); + + /// + /// Adds a text string to this path bounded by structure. + /// + public void AddString(string text, FontFamily family, int style, float emSize, Rectangle layout, StringFormat format) + => AddString(text, family, style, emSize, layout.m_rect, format); + + /// + /// Adds a text string to this path starting from a structure. + /// + public void AddString(string text, FontFamily family, int style, float emSize, PointF origin, StringFormat format) + => AddString(text, family, style, emSize, new RectangleF(origin, float.MaxValue, float.MaxValue), format); + + /// + /// Adds a text string to this path starting from a structure. + /// + public void AddString(string text, FontFamily family, int style, float emSize, Point origin, StringFormat format) + => AddString(text, family, style, emSize, new Rectangle(origin, int.MaxValue, int.MaxValue), format); + + /// + /// Clears all markers from this path. + /// + public void ClearMarkers() + => throw new NotSupportedException("skia unsupported feature"); + + /// + /// Closes all open figures in this path and starts a new figure. It closes + /// each open figure by connecting a line from its endpoint to its starting point. + /// + public void CloseAllFigures() + => throw new NotSupportedException("skia unsupported feature"); + + /// + /// Closes the current figure and starts a new figure. If the current figure + /// contains a sequence of connected lines and curves, the method closes the + /// loop by connecting a line from the endpoint to the starting point. + /// + public void CloseFigure() + => m_path.Close(); + + /// + /// Converts each curve in this path into a sequence of connected line segments. + /// + public void Flatten() + => Flatten(new Matrix()); + + /// + /// Applies the specified transform and then converts each curve + /// in this into a sequence of connected + /// line segments. + /// + public void Flatten(Matrix matrix, float flatness = 0.25f) + => throw new NotImplementedException("skia unsupported feature"); + + /// + /// Returns a rectangle that bounds this . + /// + public RectangleF GetBounds() + => GetBounds(new Matrix()); + + /// + /// Returns a rectangle that bounds this + /// when this path is transformed by the specified . + /// + public RectangleF GetBounds(Matrix matrix) + => GetBounds(matrix, null); + + /// + /// Returns a rectangle that bounds this + /// when the current path is transformed by the specified and + /// drawn with the specified . + /// + public RectangleF GetBounds(Matrix matrix, object pen) + => throw new NotImplementedException(); + + /// + /// Gets the last point in the array of this . + /// + public PointF GetLastPoint() + => new(m_path.LastPoint); + + /// + /// Indicates whether the specified point is contained within (under) the outline of + /// this when drawn with the specified and + /// using the specified . + /// + public bool IsOutlineVisible(float x, float y, object pen, object g = null) + => IsOutlineVisible(new PointF(x, y), pen, g); + + /// + /// Indicates whether the specified structure is contained + /// within (under) the outline of this when drawn + /// with the specified and using the specified . + /// + public bool IsOutlineVisible(PointF point, object pen, object g = null) + => IsOutlineVisible(point.m_point, null, null); + + /// + /// Indicates whether the specified point is contained within (under) the outline of + /// this when drawn with the specified and + /// using the specified . + /// + public bool IsOutlineVisible(int x, int y, object pen, object g = null) + => IsOutlineVisible(new Point(x, y), pen, g); + + /// + /// Indicates whether the specified structure is contained + /// within (under) the outline of this when drawn + /// with the specified and using the specified . + /// + public bool IsOutlineVisible(Point point, object pen, object g = null) + => IsOutlineVisible(point.m_point, new SKPaint(), null); + + /// + /// Indicates whether the specified point's coordinate is contained + /// within this . + /// + public bool IsVisible(float x, float y, object g = null) + => IsVisible(new PointF(x, y), g); + + /// + /// Indicates whether the specified structure is + /// contained within this . + /// + public bool IsVisible(PointF point, object g = null) // TODO: consider Graphics + => IsVisible(point.m_point, null); + + /// + /// Indicates whether the specified point's coordinate is contained + /// within this . + /// + public bool IsVisible(int x, int y, object g = null) + => IsVisible(new Point(x, y), g); + + /// + /// Indicates whether the specified structure is + /// contained within this . + /// + public bool IsVisible(Point point, object g = null) // TODO: consider Graphics + => IsVisible(point.m_point, null); + + /// + /// Empties the and arrays + /// and sets the to . + /// + public void Reset() + => m_path.Reset(); + + /// + /// Reverses the order of points in the array of this . + /// + public void Reverse() + { + var path = new SKPath(m_path); + m_path.Reset(); + m_path.AddPathReverse(path); + } + + /// + /// Sets a marker on this . + /// + public void SetMarkers() + => throw new NotImplementedException(); + + /// + /// Starts a new figure without closing the current figure. All subsequent + /// points added to the path are added to this new figure. + /// + public void StartFigure() + => throw new NotImplementedException(); + + /// + /// Applies a transform matrix to this . + /// + public void Transform(Matrix matrix) + => m_path.Transform(matrix.m_matrix); + + /// + /// Applies a warp transform to this , defined by a , + /// a parallelogram (serie of structure), a , and a flatness + /// value for curves. + /// + public void Warp(PointF[] destPoints, RectangleF srcRect, Matrix matrix = null, WarpMode warpMode = WarpMode.Perspective, float flatness = 0.25f) + => throw new NotImplementedException(); + + /// + /// Replaces this with curves that enclose the area that is + /// filled when this path is drawn by the specified and flatness + /// value for curves. + /// + public void Widen(object pen, Matrix matrix = null, float flatness = 0.25f) + => throw new NotImplementedException(); + + #endregion + + + #region Skia + + private void AddArc(SKRect rect, float startAngle, float sweepAngle) + => m_path.AddArc(rect, startAngle, sweepAngle); + + + private void AddBezier(SKPoint pt1, SKPoint pt2, SKPoint pt3, SKPoint pt4) + => m_path.CubicTo(pt1, pt2, pt3); + + + private void AddBeziers(params SKPoint[] points) + { + if (points.Length % 4 != 0) + throw new ArgumentException($"beziers requires points lenght with multiple of 4", nameof(points)); + for (int i = 0; i < points.Length; i += 4) + AddBezier(points[i], points[i + 1], points[i + 2], points[i + 4]); + } + + + public void AddCurve(SKPoint[] points, float tension, bool closed) // TODO: implement tension + => m_path.AddPoly(points, closed); + + + public void AddEllipse(SKRect rect) + => m_path.AddOval(rect); + + + public void AddLine(SKPoint pt1, SKPoint pt2) + { + m_path.MoveTo(pt1); + m_path.LineTo(pt2); + } + + + public void AddPie(SKRect rect, float startAngle, float sweepAngle) + { + m_path.AddArc(rect, startAngle, sweepAngle); + m_path.LineTo(rect.MidX, rect.MidY); + m_path.Close(); + } + + + public void AddPolygon(SKPoint[] points) + { + if (points.Length < 3) + throw new ArgumentException("At least three points are required."); + m_path.AddPoly(points, true); + } + + + public void AddRectangle(SKRect rect) + => m_path.AddRect(rect); + + + public void AddString(string text, FontFamily family, int style, float emSize, SKRect layout, StringFormat format) + { + format ??= new StringFormat(); + + bool isRightToLeft = format.FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft); + + using var paint = new SKPaint + { + Typeface = family.GetTypeface((FontStyle)style), + TextSize = emSize, + TextAlign = format.Alignment switch + { + StringAlignment.Near => isRightToLeft ? SKTextAlign.Right : SKTextAlign.Left, + StringAlignment.Far => isRightToLeft ? SKTextAlign.Left : SKTextAlign.Right, + StringAlignment.Center => SKTextAlign.Center, + _ => throw new ArgumentException($"invalid {format.Alignment} text alignment.", nameof(format)) + }, + Style = SKPaintStyle.Stroke + }; + + float baselineHeight = -paint.FontMetrics.Ascent; + float underlineOffset = paint.FontMetrics.UnderlinePosition ?? 1.8f; + float underlineHeight = paint.FontMetrics.UnderlineThickness ?? paint.GetTextPath("_", 0, 0).Bounds.Height; + float paragraphOffset = paint.FontSpacing - baselineHeight - underlineHeight - underlineOffset; + + // define offset based on System.Drawing (based on trial/error comparison) + float offsetX = isRightToLeft ? 0 : 5, offsetY = baselineHeight - 6; + + // apply format to the string + text = format.ApplyDirection(text); + text = format.ApplyTabStops(text); + text = format.ApplyControlEscape(text); + text = format.ApplyWrapping(text, layout, paint.MeasureText); + text = format.ApplyTrimming(text, layout, paint.FontSpacing, paint.MeasureText); + text = format.ApplyHotkey(text, out var underlines); + + // create returning path + var path = new SKPath(); + + // get path for current text, including breaklines and underlines + float lineHeightOffset = 0f; + int lineIndexOffset = 0; + int breaklineOffset = format.FormatFlags.HasFlag(StringFormatFlags.NoWrap) ? 1 : 0; + foreach (string line in text.Split(StringFormat.BREAKLINES, StringSplitOptions.None)) + { + // check if the line fits within the layout height + if (format.FormatFlags.HasFlag(StringFormatFlags.LineLimit) && lineHeightOffset + baselineHeight > layout.Height) + break; + + // get text path for current line + var linePath = paint.GetTextPath(line, 0, lineHeightOffset); + + // Adjust horizontal alignments for right-to-left text + float rtlOffset = isRightToLeft ? layout.Width - paint.MeasureText(line) - 5 : 0; + linePath.Offset(rtlOffset, 0); + + // get rect path for each underline defined by hotkey prefix + foreach (var index in underlines.Where(idx => idx >= lineIndexOffset && idx < lineIndexOffset + line.Length)) + { + int relIndex = index - lineIndexOffset; + if (isRightToLeft && relIndex == 0 && line[relIndex] == '…') + relIndex += 1; // TODO: look for a better fix for this (in rtl) + float origin = paint.MeasureText(line.Substring(0, relIndex)); + float length = paint.MeasureText(line.Substring(relIndex, 1)); + var underline = new SKRect( + origin + rtlOffset, + lineHeightOffset + underlineOffset, + origin + length + rtlOffset, + lineHeightOffset + underlineOffset + underlineHeight); + linePath.AddRect(underline); + } + + // add line path + path.AddPath(linePath); + + lineIndexOffset += line.Length + breaklineOffset; + lineHeightOffset += baselineHeight + paragraphOffset; + } + + // align path vertically + if (format.LineAlignment == StringAlignment.Center) + path.Offset(0, (layout.Height - lineHeightOffset) / 2); + else if (format.LineAlignment == StringAlignment.Far) + path.Offset(0, layout.Height - lineHeightOffset); + + // apply fit if required + if (format.FormatFlags.HasFlag(StringFormatFlags.FitBlackBox)) + { + float fitOffsetX = isRightToLeft + ? Math.Min(0, layout.Right - path.Bounds.Right) + : Math.Max(0, layout.Left - path.Bounds.Left); + path.Offset(fitOffsetX, 0); + } + + // apply offset reltive to layout + path.Offset(layout.Left + offsetX, layout.Top + offsetY); + + // apply rotation and offset if required + if (format.FormatFlags.HasFlag(StringFormatFlags.DirectionVertical)) + { + path.Transform(SKMatrix.CreateRotationDegrees(90)); + path.Offset(path.Bounds.Standardized.Height + paragraphOffset + underlineOffset + underlineHeight, 0); + } + + // apply clip if required + if (!format.FormatFlags.HasFlag(StringFormatFlags.NoClip)) + { + var bounds = new SKPath(); + bounds.AddRect(layout); + path = path.Op(bounds, SKPathOp.Intersect); + } + + m_path.AddPath(path); + } + + + public bool IsOutlineVisible(SKPoint point, SKPaint pen, SKRect? bounds) + { + bool isBoundContained = bounds?.Contains(point) ?? true; + return isBoundContained && pen.GetFillPath(m_path).Contains(point.X, point.Y); + } + + + public bool IsVisible(SKPoint point, SKRect? bounds) + { + bool isBoundContained = bounds?.Contains(point) ?? true; + return isBoundContained && m_path.Contains(point.X, point.Y); + } + + #endregion + + + #region Utilities + + private static SKPath CreatePath(PointF[] points, byte[] types, FillMode mode) + { + if (points.Length != types.Length) + throw new ArgumentException("points and types arrays must have the same length."); + + var path = new SKPath() + { + FillType = mode switch + { + FillMode.Alternate => SKPathFillType.EvenOdd, + FillMode.Winding => SKPathFillType.Winding, + _ => throw new ArgumentException($"invlid value {mode}.", nameof(mode)) + } + }; + + for (int i = 0; i < points.Length; i++) + { + switch ((PathPointType)types[i]) + { + case PathPointType.Start: + path.MoveTo(points[i].m_point); + break; + + case PathPointType.Line: + path.LineTo(points[i].m_point); + break; + + case PathPointType.Bezier: + if (i + 2 >= points.Length || (PathPointType)types[i + 1] != PathPointType.Bezier || (PathPointType)types[i + 2] != PathPointType.Bezier) + throw new ArgumentException("Invalid Bezier curve definition."); + path.CubicTo(points[i].m_point, points[i + 1].m_point, points[i + 2].m_point); + i += 2; + break; + + case PathPointType.CloseSubpath: + path.Close(); + break; + + case PathPointType.PathTypeMask: + throw new NotImplementedException(); + + case PathPointType.PathMarker: + throw new NotImplementedException(); + } + } + + return path; + } + + #endregion +} diff --git a/src/Common/Drawing2D/PathData.cs b/src/Common/Drawing2D/PathData.cs new file mode 100644 index 0000000..b81b12e --- /dev/null +++ b/src/Common/Drawing2D/PathData.cs @@ -0,0 +1,25 @@ +namespace GeneXus.Drawing.Drawing2D; + +public sealed class PathData +{ + + /// + /// Initializes a new instance of the class. + /// + public PathData() { } + + + #region Properties + + /// + /// Gets or sets an array of structures that represents the points through which the path is constructed. + /// + public PointF[] Points { get; set; } + + /// + /// Gets or sets the types of the corresponding points in the path. + /// + public byte[] Types { get; set; } + + #endregion +} \ No newline at end of file diff --git a/src/Common/Drawing2D/PathPointType.cs b/src/Common/Drawing2D/PathPointType.cs new file mode 100644 index 0000000..9045710 --- /dev/null +++ b/src/Common/Drawing2D/PathPointType.cs @@ -0,0 +1,47 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the type of point in a object. +/// +public enum PathPointType +{ + /// + /// Indicates that the point is the start of a figure. + /// + Start = 0, + + /// + /// Indicates that the point is an endpoint of a line. + /// + Line = 1, + + /// + /// Indicates that the point is an endpoint or a control point of a cubic Bézier spline. + /// + Bezier = 3, + + /// + /// Masks all bits except for the three low-order bits, which indicate the point type. + /// + PathTypeMask = 7, + + /// + /// Not used. + /// + DashMode = 16, + + /// + /// Specifies that the point is a marker. + /// + PathMarker = 32, + + /// + /// Specifies that the point is the last point in a closed subpath (figure). + /// + CloseSubpath = 128, + + /// + /// A cubic Bézier curve. + /// + Bezier3 = Bezier +} diff --git a/src/Common/Drawing2D/WarpMode.cs b/src/Common/Drawing2D/WarpMode.cs new file mode 100644 index 0000000..1c10419 --- /dev/null +++ b/src/Common/Drawing2D/WarpMode.cs @@ -0,0 +1,17 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the type of warp transformation applied in a method. +/// +public enum WarpMode +{ + /// + /// Specifies a perspective warp. + /// + Perspective = 0, + + /// + /// Specifies a bilinear warp. + /// + Bilinear = 1 +} From d585997e011cb59594f99ed3aec779d7333ec903 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 20:13:51 -0300 Subject: [PATCH 004/217] add Brush class --- src/Common/Brush.cs | 68 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/Common/Brush.cs diff --git a/src/Common/Brush.cs b/src/Common/Brush.cs new file mode 100644 index 0000000..1558e78 --- /dev/null +++ b/src/Common/Brush.cs @@ -0,0 +1,68 @@ +using System; +using SkiaSharp; + +namespace GeneXus.Drawing; + +public abstract class Brush : IDisposable, ICloneable +{ + internal readonly SKPaint m_paint; + + internal Brush(SKPaint paint) + { + m_paint = paint ?? throw new ArgumentNullException(nameof(paint)); + m_paint.Style = SKPaintStyle.Fill; + } + + /// + /// Cleans up resources for this . + /// + ~Brush() => Dispose(false); + + + #region IDisposable + + /// + /// Cleans up resources for this . + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + protected virtual void Dispose(bool disposing) => m_paint.Dispose(); + + #endregion + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public abstract object Clone(); + + #endregion + + + #region Operators + + /// + /// Creates a with the coordinates of the specified . + /// + public static explicit operator SKPaint(Brush brush) => brush.m_paint; + + #endregion + + + #region Utilities + + protected static Color ApplyFactor(Color color, float factor) + => Color.FromArgb( + (int)(color.A * factor), + (int)(color.R * factor), + (int)(color.G * factor), + (int)(color.B * factor)); + + #endregion +} From 4669b61243887f14c49f883bbd4c4d421f3cd76c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 20:15:18 -0300 Subject: [PATCH 005/217] add SolidBrush class --- src/Common/SolidBrush.cs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/Common/SolidBrush.cs diff --git a/src/Common/SolidBrush.cs b/src/Common/SolidBrush.cs new file mode 100644 index 0000000..cf791a5 --- /dev/null +++ b/src/Common/SolidBrush.cs @@ -0,0 +1,35 @@ + +using SkiaSharp; + +namespace GeneXus.Drawing; + +public sealed class SolidBrush : Brush +{ + public SolidBrush(Color color) + : base(new SKPaint { Color = color.m_color }) { } + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public override object Clone() + => new SolidBrush(new Color(m_paint.Color)); + + #endregion + + + #region Properties + + /// + /// Gets or sets the color of this object. + /// + public Color Color + { + get => new(m_paint.Color); + set => m_paint.Color = value.m_color; + } + + #endregion +} From f02453e110062bb82b650859ea1d494c7fc90af5 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 20:16:30 -0300 Subject: [PATCH 006/217] add Brushes class --- src/Common/Brushes.cs | 168 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/Common/Brushes.cs diff --git a/src/Common/Brushes.cs b/src/Common/Brushes.cs new file mode 100644 index 0000000..e96bb40 --- /dev/null +++ b/src/Common/Brushes.cs @@ -0,0 +1,168 @@ + + +namespace GeneXus.Drawing; + +public static class Brushes +{ + public static Brush Transparent => new SolidBrush(Color.Transparent); + + public static Brush AliceBlue => new SolidBrush(Color.AliceBlue); + public static Brush AntiqueWhite => new SolidBrush(Color.AntiqueWhite); + public static Brush Aqua => new SolidBrush(Color.Aqua); + public static Brush Aquamarine => new SolidBrush(Color.Aquamarine); + public static Brush Azure => new SolidBrush(Color.Azure); + + public static Brush Beige => new SolidBrush(Color.Beige); + public static Brush Bisque => new SolidBrush(Color.Bisque); + public static Brush Black => new SolidBrush(Color.Black); + public static Brush BlanchedAlmond => new SolidBrush(Color.BlanchedAlmond); + public static Brush Blue => new SolidBrush(Color.Blue); + public static Brush BlueViolet => new SolidBrush(Color.BlueViolet); + public static Brush Brown => new SolidBrush(Color.Brown); + public static Brush BurlyWood => new SolidBrush(Color.BurlyWood); + + public static Brush CadetBlue => new SolidBrush(Color.CadetBlue); + public static Brush Chartreuse => new SolidBrush(Color.Chartreuse); + public static Brush Chocolate => new SolidBrush(Color.Chocolate); + public static Brush Coral => new SolidBrush(Color.Coral); + public static Brush CornflowerBlue => new SolidBrush(Color.CornflowerBlue); + public static Brush Cornsilk => new SolidBrush(Color.Cornsilk); + public static Brush Crimson => new SolidBrush(Color.Crimson); + public static Brush Cyan => new SolidBrush(Color.Cyan); + + public static Brush DarkBlue => new SolidBrush(Color.DarkBlue); + public static Brush DarkCyan => new SolidBrush(Color.DarkCyan); + public static Brush DarkGoldenrod => new SolidBrush(Color.DarkGoldenrod); + public static Brush DarkGray => new SolidBrush(Color.DarkGray); + public static Brush DarkGreen => new SolidBrush(Color.DarkGreen); + public static Brush DarkKhaki => new SolidBrush(Color.DarkKhaki); + public static Brush DarkMagenta => new SolidBrush(Color.DarkMagenta); + public static Brush DarkOliveGreen => new SolidBrush(Color.DarkOliveGreen); + public static Brush DarkOrange => new SolidBrush(Color.DarkOrange); + public static Brush DarkOrchid => new SolidBrush(Color.DarkOrchid); + public static Brush DarkRed => new SolidBrush(Color.DarkRed); + public static Brush DarkSalmon => new SolidBrush(Color.DarkSalmon); + public static Brush DarkSeaGreen => new SolidBrush(Color.DarkSeaGreen); + public static Brush DarkSlateBlue => new SolidBrush(Color.DarkSlateBlue); + public static Brush DarkSlateGray => new SolidBrush(Color.DarkSlateGray); + public static Brush DarkTurquoise => new SolidBrush(Color.DarkTurquoise); + public static Brush DarkViolet => new SolidBrush(Color.DarkViolet); + public static Brush DeepPink => new SolidBrush(Color.DeepPink); + public static Brush DeepSkyBlue => new SolidBrush(Color.DeepSkyBlue); + public static Brush DimGray => new SolidBrush(Color.DimGray); + public static Brush DodgerBlue => new SolidBrush(Color.DodgerBlue); + + public static Brush Firebrick => new SolidBrush(Color.Firebrick); + public static Brush FloralWhite => new SolidBrush(Color.FloralWhite); + public static Brush ForestGreen => new SolidBrush(Color.ForestGreen); + public static Brush Fuchsia => new SolidBrush(Color.Fuchsia); + + public static Brush Gainsboro => new SolidBrush(Color.Gainsboro); + public static Brush GhostWhite => new SolidBrush(Color.GhostWhite); + public static Brush Gold => new SolidBrush(Color.Gold); + public static Brush Goldenrod => new SolidBrush(Color.Goldenrod); + public static Brush Gray => new SolidBrush(Color.Gray); + public static Brush Green => new SolidBrush(Color.Green); + public static Brush GreenYellow => new SolidBrush(Color.GreenYellow); + + public static Brush Honeydew => new SolidBrush(Color.Honeydew); + public static Brush HotPink => new SolidBrush(Color.HotPink); + + public static Brush IndianRed => new SolidBrush(Color.IndianRed); + public static Brush Indigo => new SolidBrush(Color.Indigo); + public static Brush Ivory => new SolidBrush(Color.Ivory); + + public static Brush Khaki => new SolidBrush(Color.Khaki); + + public static Brush Lavender => new SolidBrush(Color.Lavender); + public static Brush LavenderBlush => new SolidBrush(Color.LavenderBlush); + public static Brush LawnGreen => new SolidBrush(Color.LawnGreen); + public static Brush LemonChiffon => new SolidBrush(Color.LemonChiffon); + public static Brush LightBlue => new SolidBrush(Color.LightBlue); + public static Brush LightCoral => new SolidBrush(Color.LightCoral); + public static Brush LightCyan => new SolidBrush(Color.LightCyan); + public static Brush LightGoldenrodYellow => new SolidBrush(Color.LightGoldenrodYellow); + public static Brush LightGreen => new SolidBrush(Color.LightGreen); + public static Brush LightGray => new SolidBrush(Color.LightGray); + public static Brush LightPink => new SolidBrush(Color.LightPink); + public static Brush LightSalmon => new SolidBrush(Color.LightSalmon); + public static Brush LightSeaGreen => new SolidBrush(Color.LightSeaGreen); + public static Brush LightSkyBlue => new SolidBrush(Color.LightSkyBlue); + public static Brush LightSlateGray => new SolidBrush(Color.LightSlateGray); + public static Brush LightSteelBlue => new SolidBrush(Color.LightSteelBlue); + public static Brush LightYellow => new SolidBrush(Color.LightYellow); + public static Brush Lime => new SolidBrush(Color.Lime); + public static Brush LimeGreen => new SolidBrush(Color.LimeGreen); + public static Brush Linen => new SolidBrush(Color.Linen); + + public static Brush Magenta => new SolidBrush(Color.Magenta); + public static Brush Maroon => new SolidBrush(Color.Maroon); + public static Brush MediumAquamarine => new SolidBrush(Color.MediumAquamarine); + public static Brush MediumBlue => new SolidBrush(Color.MediumBlue); + public static Brush MediumOrchid => new SolidBrush(Color.MediumOrchid); + public static Brush MediumPurple => new SolidBrush(Color.MediumPurple); + public static Brush MediumSeaGreen => new SolidBrush(Color.MediumSeaGreen); + public static Brush MediumSlateBlue => new SolidBrush(Color.MediumSlateBlue); + public static Brush MediumSpringGreen => new SolidBrush(Color.MediumSpringGreen); + public static Brush MediumTurquoise => new SolidBrush(Color.MediumTurquoise); + public static Brush MediumVioletRed => new SolidBrush(Color.MediumVioletRed); + public static Brush MidnightBlue => new SolidBrush(Color.MidnightBlue); + public static Brush MintCream => new SolidBrush(Color.MintCream); + public static Brush MistyRose => new SolidBrush(Color.MistyRose); + public static Brush Moccasin => new SolidBrush(Color.Moccasin); + + public static Brush NavajoWhite => new SolidBrush(Color.NavajoWhite); + public static Brush Navy => new SolidBrush(Color.Navy); + + public static Brush OldLace => new SolidBrush(Color.OldLace); + public static Brush Olive => new SolidBrush(Color.Olive); + public static Brush OliveDrab => new SolidBrush(Color.OliveDrab); + public static Brush Orange => new SolidBrush(Color.Orange); + public static Brush OrangeRed => new SolidBrush(Color.OrangeRed); + public static Brush Orchid => new SolidBrush(Color.Orchid); + + public static Brush PaleGoldenrod => new SolidBrush(Color.PaleGoldenrod); + public static Brush PaleGreen => new SolidBrush(Color.PaleGreen); + public static Brush PaleTurquoise => new SolidBrush(Color.PaleTurquoise); + public static Brush PaleVioletRed => new SolidBrush(Color.PaleVioletRed); + public static Brush PapayaWhip => new SolidBrush(Color.PapayaWhip); + public static Brush PeachPuff => new SolidBrush(Color.PeachPuff); + public static Brush Peru => new SolidBrush(Color.Peru); + public static Brush Pink => new SolidBrush(Color.Pink); + public static Brush Plum => new SolidBrush(Color.Plum); + public static Brush PowderBlue => new SolidBrush(Color.PowderBlue); + public static Brush Purple => new SolidBrush(Color.Purple); + + public static Brush Red => new SolidBrush(Color.Red); + public static Brush RosyBrown => new SolidBrush(Color.RosyBrown); + public static Brush RoyalBlue => new SolidBrush(Color.RoyalBlue); + + public static Brush SaddleBrown => new SolidBrush(Color.SaddleBrown); + public static Brush Salmon => new SolidBrush(Color.Salmon); + public static Brush SandyBrown => new SolidBrush(Color.SandyBrown); + public static Brush SeaGreen => new SolidBrush(Color.SeaGreen); + public static Brush SeaShell => new SolidBrush(Color.SeaShell); + public static Brush Sienna => new SolidBrush(Color.Sienna); + public static Brush Silver => new SolidBrush(Color.Silver); + public static Brush SkyBlue => new SolidBrush(Color.SkyBlue); + public static Brush SlateBlue => new SolidBrush(Color.SlateBlue); + public static Brush SlateGray => new SolidBrush(Color.SlateGray); + public static Brush Snow => new SolidBrush(Color.Snow); + public static Brush SpringGreen => new SolidBrush(Color.SpringGreen); + public static Brush SteelBlue => new SolidBrush(Color.SteelBlue); + + public static Brush Tan => new SolidBrush(Color.Tan); + public static Brush Teal => new SolidBrush(Color.Teal); + public static Brush Thistle => new SolidBrush(Color.Thistle); + public static Brush Tomato => new SolidBrush(Color.Tomato); + public static Brush Turquoise => new SolidBrush(Color.Turquoise); + + public static Brush Violet => new SolidBrush(Color.Violet); + + public static Brush Wheat => new SolidBrush(Color.Wheat); + public static Brush White => new SolidBrush(Color.White); + public static Brush WhiteSmoke => new SolidBrush(Color.WhiteSmoke); + + public static Brush Yellow => new SolidBrush(Color.Yellow); + public static Brush YellowGreen => new SolidBrush(Color.YellowGreen); +} From 99444862f2001ee161286085d0be3803ea4f47e9 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 20:21:14 -0300 Subject: [PATCH 007/217] add TextureBrush class and WrapMode enum --- src/Common/Drawing2D/WrapMode.cs | 32 +++++++ src/Common/TextureBrush.cs | 152 +++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 src/Common/Drawing2D/WrapMode.cs create mode 100644 src/Common/TextureBrush.cs diff --git a/src/Common/Drawing2D/WrapMode.cs b/src/Common/Drawing2D/WrapMode.cs new file mode 100644 index 0000000..8c545b0 --- /dev/null +++ b/src/Common/Drawing2D/WrapMode.cs @@ -0,0 +1,32 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies how a texture or gradient is tiled when it is smaller than the area being filled. +/// +public enum WrapMode +{ + /// + /// Tiles the gradient or texture. + /// + Tile = 0, + + /// + /// Reverses the texture or gradient horizontally and then tiles the texture or gradient. + /// + TileFlipX = 1, + + /// + /// Reverses the texture or gradient vertically and then tiles the texture or gradient. + /// + TileFlipY = 2, + + /// + /// Reverses the texture or gradient horizontally and vertically and then tiles the texture or gradient. + /// + TileFlipXY = 3, + + /// + /// The texture or gradient is not tiled. + /// + Clamp = 4 +} diff --git a/src/Common/TextureBrush.cs b/src/Common/TextureBrush.cs new file mode 100644 index 0000000..fa4e339 --- /dev/null +++ b/src/Common/TextureBrush.cs @@ -0,0 +1,152 @@ +using System; +using SkiaSharp; +using GeneXus.Drawing.Drawing2D; + +namespace GeneXus.Drawing; + +public sealed class TextureBrush : Brush +{ + internal Bitmap m_bitmap; + internal Rectangle m_rect; + internal WrapMode m_mode; + + private TextureBrush(Rectangle rect, Bitmap bitmap, WrapMode mode) + : base(new SKPaint { Shader = CreateShader(bitmap, rect, mode) }) + { + m_rect = rect; + m_bitmap = bitmap; + m_mode = mode; + } + + /// + /// Initializes a new object that uses the + /// specified image, wrap mode, and bounding rectangle. + /// + public TextureBrush(Bitmap bitmap, Rectangle rect, WrapMode mode = WrapMode.Tile) + : this(rect, bitmap, mode) { } + + /// + /// Initializes a new object that uses the + /// specified image and wrap mode. + /// + public TextureBrush(Bitmap bitmap, WrapMode mode = WrapMode.Tile) + : this(bitmap, new Rectangle(0, 0, bitmap.Size), mode) { } + + /// + /// Initializes a new object that uses + /// the specified image, bounding rectangle, and image attributes. + /// + public TextureBrush(Bitmap bitmap, Rectangle rect, object imageAttributes) + : this(bitmap, rect, WrapMode.Tile) { } // TODO: implement ImageAttributes class + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public override object Clone() + => new TextureBrush(m_rect, m_bitmap, m_mode); + + #endregion + + + #region Properties + + /// + /// Gets the object associated with + /// this object. + /// + public Image Image => m_bitmap; + + /// + /// Gets or sets a copy of the object + /// that defines a local geometric transformation for the image associated + /// with this object. + /// + public Matrix Transform { get; set; } = new Matrix(); + + /// + /// Gets or sets a enumeration that + /// indicates the wrap mode for this object. + /// + public WrapMode WrapMode + { + get => m_mode; + set => m_mode = value; // TODO: update shader when updating this + } + + #endregion + + + #region Methods + + /// + /// Multiplies the object that represents the local + /// geometric transformation of this object + /// by the specified object in the specified order. + /// + public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend) + => Transform.Multiply(matrix, order); + + /// + /// Resets the Transform property of this object to identity. + /// + public void ResetTransform() + => Transform.Reset(); + + /// + /// Rotates the local geometric transformation of this object + /// by the specified amount in the specified order. + /// + public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend) + => Transform.Rotate(angle, order); + + /// + /// Scales the local geometric transformation of this object + /// by the specified amounts in the specified order. + /// + public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend) + => Transform.Scale(sx, sy, order); + + /// + /// Translates the local geometric transformation of this object + /// by the specified dimensions in the specified order. + /// + public void TranslateTransform(float dx, float dy, MatrixOrder order) + => Transform.Translate(dx, dy, order); + + #endregion + + + #region Utilities + + private static SKShader CreateShader(Bitmap bitmap, Rectangle rect, WrapMode mode) + { + /* + * NOTE: For mapping WrapMode.Clamp + * - SKShaderTileMode.Clamp: Replicate the edge color if the shader draws outside of its original bounds + * - SKShaderTileMode.Decal: Only draw within the original domain, return transparent-black everywhere else. + */ + + (var tmx, var tmy) = mode switch + { + WrapMode.Tile => (SKShaderTileMode.Repeat, SKShaderTileMode.Repeat), + WrapMode.Clamp => (SKShaderTileMode.Decal, SKShaderTileMode.Decal), + WrapMode.TileFlipX => (SKShaderTileMode.Mirror, SKShaderTileMode.Repeat), + WrapMode.TileFlipY => (SKShaderTileMode.Repeat, SKShaderTileMode.Mirror), + WrapMode.TileFlipXY => (SKShaderTileMode.Mirror, SKShaderTileMode.Mirror), + _ => throw new NotImplementedException() + }; + + var info = new SKImageInfo((int)rect.Width, (int)rect.Height); + + using var surfece = SKSurface.Create(info); + surfece.Canvas.DrawBitmap(bitmap.m_bitmap, rect.m_rect); + + var src = surfece.Snapshot(); + return SKShader.CreateImage(src, tmx, tmy); + } + + #endregion +} From 9f6c0e99242948f9ab611ec2a450b4f08082d6e4 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 20:24:10 -0300 Subject: [PATCH 008/217] add HatchBrush style and HatchStyle enum --- src/Common/Drawing2D/HatchBrush.cs | 342 +++++++++++++++++++++++++++++ src/Common/Drawing2D/HatchStyle.cs | 287 ++++++++++++++++++++++++ 2 files changed, 629 insertions(+) create mode 100644 src/Common/Drawing2D/HatchBrush.cs create mode 100644 src/Common/Drawing2D/HatchStyle.cs diff --git a/src/Common/Drawing2D/HatchBrush.cs b/src/Common/Drawing2D/HatchBrush.cs new file mode 100644 index 0000000..eccdb96 --- /dev/null +++ b/src/Common/Drawing2D/HatchBrush.cs @@ -0,0 +1,342 @@ +using System; +using SkiaSharp; + +namespace GeneXus.Drawing.Drawing2D; + +public sealed class HatchBrush : Brush +{ + private readonly Color m_fore; + private readonly Color m_back; + private readonly HatchStyle m_style; + + public HatchBrush(HatchStyle hatchStyle, Color foreColor, Color backColor) + : base(new SKPaint { Shader = CreateShader(hatchStyle, foreColor, backColor) }) + { + m_fore = foreColor; + m_back = backColor; + m_style = hatchStyle; + } + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public override object Clone() + => new HatchBrush(m_style, m_fore, m_back); + + #endregion + + + #region Properties + + /// + /// Gets the color of spaces between the hatch lines drawn by this object. + /// + public Color BackgroundColor => m_back; + + /// + /// Gets the color of hatch lines drawn by this object. + /// + public Color ForegroundColor => m_fore; + + /// + /// Gets the hatch style of this object. + /// + public HatchStyle HatchStyle => m_style; + + #endregion + + + #region Utilities + + private static SKShader CreateShader(HatchStyle style, Color foreColor, Color backColor) + { + var size = 10f; + using var pattern = new SKBitmap((int)size, (int)size); + + using var canvas = new SKCanvas(pattern); + canvas.Clear(backColor.m_color); + + var paint = new SKPaint + { + Color = foreColor.m_color, + Style = SKPaintStyle.Stroke, + StrokeWidth = 1, + IsAntialias = true + }; + + switch (style) + { + case HatchStyle.Horizontal: + canvas.DrawLine(0, size / 2, size, size / 2, paint); + break; + + case HatchStyle.Vertical: + canvas.DrawLine(size / 2, 0, size / 2, size, paint); + break; + + case HatchStyle.ForwardDiagonal: + canvas.DrawLine(0, 0, size, size, paint); + break; + + case HatchStyle.BackwardDiagonal: + canvas.DrawLine(size, 0, 0, size, paint); + break; + + case HatchStyle.Cross: + canvas.DrawLine(0, size / 2, size, size / 2, paint); + canvas.DrawLine(size / 2, 0, size / 2, size, paint); + break; + + case HatchStyle.DiagonalCross: + canvas.DrawLine(0, 0, size, size, paint); + canvas.DrawLine(size, 0, 0, size, paint); + break; + + case HatchStyle.DashedDownwardDiagonal: + paint.PathEffect = SKPathEffect.CreateDash(new float[] { 4, 2 }, 0); + canvas.DrawLine(0, 0, size, size, paint); + break; + + case HatchStyle.DashedUpwardDiagonal: + paint.PathEffect = SKPathEffect.CreateDash(new float[] { 4, 2 }, 0); + canvas.DrawLine(0, size, size, 0, paint); + break; + + case HatchStyle.DashedHorizontal: + paint.PathEffect = SKPathEffect.CreateDash(new float[] { 4, 2 }, 0); + canvas.DrawLine(0, size / 2, size, size / 2, paint); + break; + + case HatchStyle.DashedVertical: + paint.PathEffect = SKPathEffect.CreateDash(new float[] { 4, 2 }, 0); + canvas.DrawLine(size / 2, 0, size / 2, size, paint); + break; + + case HatchStyle.SmallConfetti: + DrawConfettiPattern(canvas, paint, size, 3); + break; + + case HatchStyle.LargeConfetti: + DrawConfettiPattern(canvas, paint, size, 5); + break; + + case HatchStyle.ZigZag: + for (int i = 0; i < size; i += 2) + { + canvas.DrawLine(i, 0, i + 1, size, paint); + canvas.DrawLine(i + 1, size, i + 2, 0, paint); + } + break; + + case HatchStyle.Wave: + for (int i = 0; i < size; i += 2) + { + canvas.DrawLine(i, size / 2, i + 1, 0, paint); + canvas.DrawLine(i + 1, 0, i + 2, size / 2, paint); + canvas.DrawLine(i + 2, size / 2, i + 3, size, paint); + canvas.DrawLine(i + 3, size, i + 4, size / 2, paint); + } + break; + + case HatchStyle.Weave: + for (int i = 0; i < size; i += 4) + { + canvas.DrawLine(i, 0, i, size, paint); + canvas.DrawLine(0, i, size, i, paint); + } + break; + + case HatchStyle.Plaid: + for (int i = 0; i < size; i += 2) + { + canvas.DrawLine(i, 0, i, size, paint); + canvas.DrawLine(0, i, size, i, paint); + } + break; + + case HatchStyle.Divot: + for (int i = 0; i < size; i += 4) + { + canvas.DrawLine(i, 0, i + 2, size, paint); + canvas.DrawLine(i + 2, 0, i + 4, size, paint); + } + break; + + case HatchStyle.Shingle: + for (int i = 0; i < size; i += 4) + { + canvas.DrawLine(i, 0, i + 2, size, paint); + canvas.DrawLine(i + 2, 0, i + 4, size, paint); + } + break; + + case HatchStyle.Trellis: + for (int i = 0; i < size; i += 2) + { + canvas.DrawLine(i, 0, i, size, paint); + canvas.DrawLine(0, i, size, i, paint); + } + break; + + case HatchStyle.Sphere: + for (int i = 0; i < size; i += 4) + for (int j = 0; j < size; j += 4) + canvas.DrawCircle(i + 2, j + 2, 2, paint); + break; + + case HatchStyle.SmallGrid: + for (int i = 0; i < size; i += 2) + { + canvas.DrawLine(i, 0, i, size, paint); + canvas.DrawLine(0, i, size, i, paint); + } + break; + + case HatchStyle.DottedGrid: + for (int i = 0; i < size; i += 2) + for (int j = 0; j < size; j += 2) + canvas.DrawPoint(i, j, paint); + break; + + case HatchStyle.DottedDiamond: + for (int i = 0; i < size; i += 2) + { + canvas.DrawPoint(i, i, paint); + canvas.DrawPoint(size - i, i, paint); + } + break; + + case HatchStyle.SolidDiamond: + for (int i = 0; i < size; i += 4) + { + canvas.DrawLine(i, 0, i + 2, size, paint); + canvas.DrawLine(i + 2, 0, i + 4, size, paint); + } + break; + + case HatchStyle.OutlinedDiamond: + for (int i = 0; i < size; i += 4) + { + canvas.DrawLine(i, 0, i + 2, size, paint); + canvas.DrawLine(i + 2, 0, i + 4, size, paint); + } + break; + + case HatchStyle.DiagonalBrick: + for (int i = 0; i < size; i += 4) + { + canvas.DrawLine(i, 0, i + 2, size, paint); + canvas.DrawLine(i + 2, 0, i + 4, size, paint); + } + break; + + case HatchStyle.HorizontalBrick: + for (int i = 0; i < size; i += 4) + { + canvas.DrawLine(0, i, size, i, paint); + canvas.DrawLine(0, i + 2, size, i + 2, paint); + } + break; + + case HatchStyle.SmallCheckerBoard: + DrawCheckerBoardPattern(canvas, paint, foreColor.m_color, backColor.m_color, size, 2); + break; + + case HatchStyle.LargeCheckerBoard: + DrawCheckerBoardPattern(canvas, paint, foreColor.m_color, backColor.m_color, size, 4); + break; + + case HatchStyle.Percent05: + DrawPercentagePattern(canvas, paint, size, 5); + break; + + case HatchStyle.Percent10: + DrawPercentagePattern(canvas, paint, size, 10); + break; + + case HatchStyle.Percent20: + DrawPercentagePattern(canvas, paint, size, 20); + break; + + case HatchStyle.Percent25: + DrawPercentagePattern(canvas, paint, size, 25); + break; + + case HatchStyle.Percent30: + DrawPercentagePattern(canvas, paint, size, 30); + break; + + case HatchStyle.Percent40: + DrawPercentagePattern(canvas, paint, size, 40); + break; + + case HatchStyle.Percent50: + DrawPercentagePattern(canvas, paint, size, 50); + break; + + case HatchStyle.Percent60: + DrawPercentagePattern(canvas, paint, size, 60); + break; + + case HatchStyle.Percent70: + DrawPercentagePattern(canvas, paint, size, 70); + break; + + case HatchStyle.Percent75: + DrawPercentagePattern(canvas, paint, size, 75); + break; + + case HatchStyle.Percent80: + DrawPercentagePattern(canvas, paint, size, 80); + break; + + case HatchStyle.Percent90: + DrawPercentagePattern(canvas, paint, size, 90); + break; + + // TODO: add other styles + default: + throw new NotSupportedException($"Hatch style {style} is not supported."); + } + + return SKShader.CreateBitmap(pattern, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); + } + + private static void DrawPercentagePattern(SKCanvas canvas, SKPaint paint, float size, int percent) + { + var rectSize = (int)(size * percent / 100.0); + for (int y = 0; y < size; y += rectSize) + for (int x = 0; x < size; x += rectSize) + canvas.DrawRect(new SKRect(x, y, x + rectSize, y + rectSize), paint); + } + + private static void DrawConfettiPattern(SKCanvas canvas, SKPaint paint, float size, int pieces) + { + var isize = (int)Math.Ceiling(size); + var rand = new Random(); + for (int i = 0; i < pieces; i++) + { + int x = rand.Next(isize); + int y = rand.Next(isize); + canvas.DrawPoint(x, y, paint); + } + } + + private static void DrawCheckerBoardPattern(SKCanvas canvas, SKPaint paint, SKColor fore, SKColor back, float size, int checkerSize) + { + paint.Color = back; + for (int i = 0; i < size; i += checkerSize) + { + for (int j = 0; j < size; j += checkerSize) + { + paint.Color = paint.Color == fore ? back : fore; + canvas.DrawRect(i, j, checkerSize, checkerSize, paint); + } + } + } + + + #endregion +} diff --git a/src/Common/Drawing2D/HatchStyle.cs b/src/Common/Drawing2D/HatchStyle.cs new file mode 100644 index 0000000..6c1b399 --- /dev/null +++ b/src/Common/Drawing2D/HatchStyle.cs @@ -0,0 +1,287 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the different patterns available for HatchBrush objects. +/// +public enum HatchStyle +{ + /// + /// A pattern of horizontal lines. + /// + Horizontal = 0, + + /// + /// A pattern of vertical lines. + /// + Vertical = 1, + + /// + /// A pattern of lines on a diagonal from upper left to lower right. + /// + ForwardDiagonal = 2, + + /// + /// A pattern of lines on a diagonal from upper right to lower left. + /// + BackwardDiagonal = 3, + + /// + /// Specifies horizontal and vertical lines that cross. + /// + Cross = 4, + + /// + /// A pattern of crisscross diagonal lines. + /// + DiagonalCross = 5, + + /// + /// Specifies a 5-percent hatch. The ratio of foreground color to background color is 5:95. + /// + Percent05 = 6, + + /// + /// Specifies a 10-percent hatch. The ratio of foreground color to background color is 10:90. + /// + Percent10 = 7, + + /// + /// Specifies a 20-percent hatch. The ratio of foreground color to background color is 20:80. + /// + Percent20 = 8, + + /// + /// Specifies a 25-percent hatch. The ratio of foreground color to background color is 25:75. + /// + Percent25 = 9, + + /// + /// Specifies a 30-percent hatch. The ratio of foreground color to background color is 30:70. + /// + Percent30 = 10, + + /// + /// Specifies a 40-percent hatch. The ratio of foreground color to background color is 40:60. + /// + Percent40 = 11, + + /// + /// Specifies a 50-percent hatch. The ratio of foreground color to background color is 50:50. + /// + Percent50 = 12, + + /// + /// Specifies a 60-percent hatch. The ratio of foreground color to background color is 60:40. + /// + Percent60 = 13, + + /// + /// Specifies a 70-percent hatch. The ratio of foreground color to background color is 70:30. + /// + Percent70 = 14, + + /// + /// Specifies a 75-percent hatch. The ratio of foreground color to background color is 75:25. + /// + Percent75 = 15, + + /// + /// Specifies a 80-percent hatch. The ratio of foreground color to background color is 80:20. + /// + Percent80 = 16, + + /// + /// Specifies a 90-percent hatch. The ratio of foreground color to background color is 90:10. + /// + Percent90 = 17, + + /// + /// Specifies diagonal lines that slant to the right from top points to bottom points and are spaced 50 percent closer together than ForwardDiagonal, but are not antialiased. + /// + LightDownwardDiagonal = 18, + + /// + /// Specifies diagonal lines that slant to the left from top points to bottom points and are spaced 50 percent closer together than BackwardDiagonal, but they are not antialiased. + /// + LightUpwardDiagonal = 19, + + /// + /// Specifies diagonal lines that slant to the right from top points to bottom points, are spaced 50 percent closer together than, and are twice the width of ForwardDiagonal. This hatch pattern is not antialiased. + /// + DarkDownwardDiagonal = 20, + + /// + /// Specifies diagonal lines that slant to the left from top points to bottom points, are spaced 50 percent closer together than BackwardDiagonal, and are twice its width, but the lines are not antialiased. + /// + DarkUpwardDiagonal = 21, + + /// + /// Specifies diagonal lines that slant to the right from top points to bottom points, have the same spacing as hatch style ForwardDiagonal, and are triple its width, but are not antialiased. + /// + WideDownwardDiagonal = 22, + + /// + /// Specifies diagonal lines that slant to the left from top points to bottom points, have the same spacing as hatch style BackwardDiagonal, and are triple its width, but are not antialiased. + /// + WideUpwardDiagonal = 23, + + /// + /// Specifies vertical lines that are spaced 50 percent closer together than Vertical. + /// + LightVertical = 24, + + /// + /// Specifies horizontal lines that are spaced 50 percent closer together than Horizontal. + /// + LightHorizontal = 25, + + /// + /// Specifies vertical lines that are spaced 75 percent closer together than hatch style Vertical (or 25 percent closer together than LightVertical). + /// + NarrowVertical = 26, + + /// + /// Specifies horizontal lines that are spaced 75 percent closer together than hatch style Horizontal (or 25 percent closer together than LightHorizontal). + /// + NarrowHorizontal = 27, + + /// + /// Specifies vertical lines that are spaced 50 percent closer together than Vertical and are twice its width. + /// + DarkVertical = 28, + + /// + /// Specifies horizontal lines that are spaced 50 percent closer together than Horizontal and are twice the width of Horizontal. + /// + DarkHorizontal = 29, + + /// + /// Specifies dashed diagonal lines, that slant to the right from top points to bottom points. + /// + DashedDownwardDiagonal = 30, + + /// + /// Specifies dashed diagonal lines, that slant to the left from top points to bottom points. + /// + DashedUpwardDiagonal = 31, + + /// + /// Specifies dashed horizontal lines. + /// + DashedHorizontal = 32, + + /// + /// Specifies dashed vertical lines. + /// + DashedVertical = 33, + + /// + /// Specifies a hatch that has the appearance of confetti. + /// + SmallConfetti = 34, + + /// + /// Specifies a hatch that has the appearance of confetti, and is composed of larger pieces than SmallConfetti. + /// + LargeConfetti = 35, + + /// + /// Specifies horizontal lines that are composed of zigzags. + /// + ZigZag = 36, + + /// + /// Specifies horizontal lines that are composed of tildes. + /// + Wave = 37, + + /// + /// Specifies a hatch that has the appearance of layered bricks that slant to the left from top points to bottom points. + /// + DiagonalBrick = 38, + + /// + /// Specifies a hatch that has the appearance of horizontally layered bricks. + /// + HorizontalBrick = 39, + + /// + /// Specifies a hatch that has the appearance of a woven material. + /// + Weave = 40, + + /// + /// Specifies a hatch that has the appearance of a plaid material. + /// + Plaid = 41, + + /// + /// Specifies a hatch that has the appearance of divots. + /// + Divot = 42, + + /// + /// Specifies horizontal and vertical lines, each of which is composed of dots, that cross. + /// + DottedGrid = 43, + + /// + /// Specifies forward diagonal and backward diagonal lines, each of which is composed of dots, that cross. + /// + DottedDiamond = 44, + + /// + /// Specifies a hatch that has the appearance of diagonally layered shingles that slant to the right from top points to bottom points. + /// + Shingle = 45, + + /// + /// Specifies a hatch that has the appearance of a trellis. + /// + Trellis = 46, + + /// + /// Specifies a hatch that has the appearance of spheres laid adjacent to one another. + /// + Sphere = 47, + + /// + /// Specifies horizontal and vertical lines that cross and are spaced 50 percent closer together than hatch style Cross. + /// + SmallGrid = 48, + + /// + /// Specifies a hatch that has the appearance of a checkerboard. + /// + SmallCheckerBoard = 49, + + /// + /// Specifies a hatch that has the appearance of a checkerboard with squares that are twice the size of SmallCheckerBoard. + /// + LargeCheckerBoard = 50, + + /// + /// Specifies forward diagonal and backward diagonal lines that cross but are not antialiased. + /// + OutlinedDiamond = 51, + + /// + /// Specifies a hatch that hasthe appearance of a checkerboard placed diagonally. + /// + SolidDiamond = 52, + + /// + /// Specifies the hatch style Cross. + /// + LargeGrid = Cross, + + /// + /// Specifies hatch style Horizontal. + /// + Min = Horizontal, + + /// + /// Specifies hatch style LargeGrid. + /// + Max = LargeGrid +} From c7b826fc36b124b06cf874a2c7c0aaf93abf362a Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 20:30:55 -0300 Subject: [PATCH 009/217] add LinearGradientBrush class and referenced enum/classes --- src/Common/Drawing2D/Blend.cs | 28 ++ src/Common/Drawing2D/ColorBlend.cs | 27 ++ src/Common/Drawing2D/LinearGradientBrush.cs | 305 ++++++++++++++++++++ src/Common/Drawing2D/LinearGradientMode.cs | 27 ++ 4 files changed, 387 insertions(+) create mode 100644 src/Common/Drawing2D/Blend.cs create mode 100644 src/Common/Drawing2D/ColorBlend.cs create mode 100644 src/Common/Drawing2D/LinearGradientBrush.cs create mode 100644 src/Common/Drawing2D/LinearGradientMode.cs diff --git a/src/Common/Drawing2D/Blend.cs b/src/Common/Drawing2D/Blend.cs new file mode 100644 index 0000000..86806c3 --- /dev/null +++ b/src/Common/Drawing2D/Blend.cs @@ -0,0 +1,28 @@ +namespace GeneXus.Drawing.Drawing2D; + +public sealed class Blend +{ + + /// + /// Initializes a new instance of the class with the specified number of factors and positions. + /// + public Blend(int count = 1) + { + Factors = new float[count]; + Positions = new float[count]; + } + + #region Properties + + /// + /// Gets or sets an array of blend factors for the gradient. + /// + public float[] Factors { get; set; } + + /// + /// Gets or sets an array of blend positions for the gradient. + /// + public float[] Positions { get; set; } + + #endregion +} diff --git a/src/Common/Drawing2D/ColorBlend.cs b/src/Common/Drawing2D/ColorBlend.cs new file mode 100644 index 0000000..68da830 --- /dev/null +++ b/src/Common/Drawing2D/ColorBlend.cs @@ -0,0 +1,27 @@ +namespace GeneXus.Drawing.Drawing2D; + +public sealed class ColorBlend +{ + /// + /// Initializes a new instance of the class with the specified number of colors and positions. + /// + public ColorBlend(int count = 1) + { + Colors = new Color[count]; + Positions = new float[count]; + } + + #region Properties + + /// + /// Gets or sets an array of colors that represents the colors to use at corresponding positions along a gradient. + /// + public Color[] Colors { get; set; } + + /// + /// Gets or sets the positions along a gradient line. + /// + public float[] Positions { get; set; } + + #endregion +} diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs new file mode 100644 index 0000000..9dfe86b --- /dev/null +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -0,0 +1,305 @@ +using System; +using System.ComponentModel; +using System.Linq; +using SkiaSharp; + +namespace GeneXus.Drawing.Drawing2D; + +public sealed class LinearGradientBrush : Brush +{ + private RectangleF m_rect; + private WrapMode m_mode; + private Matrix m_transform; + private Blend m_blend; + private ColorBlend m_colors; + private bool m_gamma; + + private LinearGradientBrush(GradientVector vector, Color[] colors, WrapMode mode, Matrix transform) + : base(new SKPaint { }) + { + m_rect = new RectangleF(vector.BegPoint.X, vector.BegPoint.Y, vector.BegPoint.X + vector.EndPoint.X, vector.BegPoint.Y + vector.EndPoint.Y); + m_mode = mode; + m_transform = transform; + + m_gamma = false; + m_blend = null; + m_colors = new() + { + Colors = colors, + Positions = CreateUniformArray(colors.Length) + }; + + UpdateShader(() => { }); + } + + /// + /// Initializes a new instance of the class with the specified + /// points and colors. + /// + public LinearGradientBrush(PointF point1, PointF point2, Color color1, Color color2) + : this(GetGradientVector(point1, point2), new[] { color1, color2 }, WrapMode.Tile, new Matrix()) { } + + /// + /// Initializes a new instance of the class with the specified + /// points and colors. + /// + public LinearGradientBrush(Point point1, Point point2, Color color1, Color color2) + : this(new PointF(point1.m_point), new PointF(point2.m_point), color1, color2) { } + + /// + /// Creates a new instance of the class based on + /// a , starting and ending colors, and orientation. + /// + public LinearGradientBrush(RectangleF rect, Color color1, Color color2, LinearGradientMode mode) + : this(GetGradientVector(rect, mode), new[] { color1, color2 }, WrapMode.Tile, new Matrix()) { } + + /// + /// Creates a new instance of the class based on + /// a , starting and ending colors, and orientation. + /// + public LinearGradientBrush(Rectangle rect, Color color1, Color color2, LinearGradientMode mode) + : this(new RectangleF(rect.m_rect), color1, color2, mode) { } + + /// + /// Creates a new instance of the class based on + /// a , starting and ending colors, and an orientation angle. + /// + public LinearGradientBrush(RectangleF rect, Color color1, Color color2, float angle, bool isAngleScaleable = false) + : this(GetGradientVector(rect, angle, isAngleScaleable), new[] { color1, color2 }, WrapMode.Tile, new Matrix()) { } + + /// + /// Creates a new instance of the class based on + /// a , starting and ending colors, and an orientation angle. + /// + public LinearGradientBrush(Rectangle rect, Color color1, Color color2, float angle, bool isAngleScaleable = false) + : this(new RectangleF(rect.m_rect), color1, color2, angle, isAngleScaleable) { } + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public override object Clone() + { + var vector = GetGradientVector(m_rect, LinearGradientMode.ForwardDiagonal); + return new LinearGradientBrush(vector, m_colors.Colors, m_mode, m_transform); + } + + #endregion + + + #region Properties + + /// + /// Gets or sets a that specifies positions and factors + /// that define a custom falloff for the gradient. + /// + public Blend Blend + { + get => m_blend; + set => UpdateShader(() => m_blend = value ?? throw new ArgumentNullException(nameof(value))); + } + + /// + /// Gets or sets a value indicating whether gamma correction is enabled for + /// this . + /// + public bool GammaCorrection + { + get => m_gamma; + set => UpdateShader(() => m_gamma = value); + } + + /// + /// Gets or sets a that defines a multicolor + /// linear gradient. + /// + public ColorBlend InterpolationColors + { + get => m_colors; + set => UpdateShader(() => m_colors = value); + } + + /// + /// Gets or sets the starting and ending colors of the gradient. + /// + public Color[] LinearColors + { + get => m_colors.Colors; + set => UpdateShader(() => m_colors = new() + { + Colors = value, + Positions = CreateUniformArray(value.Length) + }); + } + + /// + /// Gets a rectangular region that defines the starting and ending points of the gradient. + /// + public RectangleF Rectangle => m_rect; + + /// + /// Gets or sets a copy of the object + /// that defines a local geometric transformation for the image associated + /// with this object. + /// + public Matrix Transform + { + get => m_transform; + set => UpdateShader(() => m_transform = value); + } + + /// + /// Gets or sets a WrapMode enumeration that indicates the wrap mode for this LinearGradientBrush. + /// + public WrapMode WrapMode + { + get => m_mode; + set => UpdateShader(() => + { + if (value is < WrapMode.Tile or > WrapMode.Clamp) + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(WrapMode)); + m_mode = value; + }); + } + + #endregion + + + #region Methods + + /// + /// Multiplies the object that represents the local + /// geometric transformation of this object + /// by the specified object in the specified order. + /// + public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend) + => UpdateShader(() => Transform.Multiply(matrix, order)); + + /// + /// Resets the Transform property of this object to identity. + /// + public void ResetTransform() + => UpdateShader(() => Transform.Reset()); + + /// + /// Rotates the local geometric transformation of this object + /// by the specified amount in the specified order. + /// + public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend) + => UpdateShader(() => Transform.Rotate(angle, order)); + + /// + /// Scales the local geometric transformation of this object + /// by the specified amounts in the specified order. + /// + public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend) + => UpdateShader(() => Transform.Scale(sx, sy, order)); + + /// + /// Translates the local geometric transformation of this object + /// by the specified dimensions in the specified order. + /// + public void TranslateTransform(float dx, float dy, MatrixOrder order) + => UpdateShader(() => Transform.Translate(dx, dy, order)); + + #endregion + + + #region Utilities + + private struct GradientVector + { + public PointF BegPoint { get; internal set; } + public PointF EndPoint { get; internal set; } + } + + private static GradientVector GetGradientVector(PointF start, PointF end) + => new() { BegPoint = start, EndPoint = end }; + + private static GradientVector GetGradientVector(RectangleF rect, LinearGradientMode mode) + { + (var begPoint, var endPoint) = mode switch + { + LinearGradientMode.Horizontal => ( new PointF(rect.Left, rect.Top), new PointF(rect.Right, rect.Top) ), + LinearGradientMode.Vertical => ( new PointF(rect.Left, rect.Top), new PointF(rect.Left, rect.Bottom) ), + LinearGradientMode.ForwardDiagonal => ( new PointF(rect.Left, rect.Top), new PointF(rect.Right, rect.Bottom) ), + LinearGradientMode.BackwardDiagonal => ( new PointF(rect.Right, rect.Top), new PointF(rect.Left, rect.Bottom) ), + _ => throw new ArgumentException($"{mode} mode is not supported.", nameof(mode)) + }; + return GetGradientVector(begPoint, endPoint); + } + + private static GradientVector GetGradientVector(RectangleF rect, float angle, bool scale) + { + double radians = angle * (Math.PI / 180); + float midWidth = rect.Width / 2f; + float midHeight = rect.Height / 2f; + + float centerX = rect.Left + midWidth; + float centerY = rect.Top + midHeight; + + float lengthX = scale ? midWidth : 1f; + float lengthY = scale ? midHeight : 1f; + + var begPoint = new PointF( + centerX - lengthX * (float)Math.Cos(radians), + centerY - lengthY * (float)Math.Sin(radians)); + + var endPoint = new PointF( + centerX + lengthX * (float)Math.Cos(radians), + centerY + lengthY * (float)Math.Sin(radians)); + + return GetGradientVector(begPoint, endPoint); + } + + private static Color ApplyGamma(Color color, float gamma) + => Color.FromArgb( + color.A, + (int)(Math.Pow(color.R / 255.0, gamma) * 255), + (int)(Math.Pow(color.G / 255.0, gamma) * 255), + (int)(Math.Pow(color.B / 255.0, gamma) * 255)); + + private static float[] CreateUniformArray(int length) + => length < 2 + ? throw new ArgumentException("at least two items are required.", nameof(length)) + : Enumerable.Range(0, length).Select(i => 1f * i / (length - 1)).ToArray(); + + private void UpdateShader(Action action) + { + action(); + + switch (m_mode) + { + case WrapMode.TileFlipX: + m_transform.Scale(-1, 1); + break; + case WrapMode.TileFlipY: + m_transform.Scale(1, -1); + break; + case WrapMode.TileFlipXY: + m_transform.Scale(-1, -1); + break; + } + + var vector = GetGradientVector(m_rect, LinearGradientMode.ForwardDiagonal); + var start = vector.BegPoint.m_point; + var end = vector.EndPoint.m_point; + var gamma = m_gamma ? 2.2f : 1.0f; + var factors = m_blend?.Factors ?? Enumerable.Repeat(1f, m_colors.Positions.Length).ToArray(); + var positions = m_colors.Positions + .Take(factors.Length) + .ToArray(); + var colors = m_colors.Colors + .Zip(factors, ApplyFactor) + .Select(color => ApplyGamma(color, gamma).m_color) + .ToArray(); + var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; + var matrix = m_transform.m_matrix; + + m_paint.Shader = SKShader.CreateLinearGradient(start, end, colors, positions, mode, matrix); + } + + #endregion +} diff --git a/src/Common/Drawing2D/LinearGradientMode.cs b/src/Common/Drawing2D/LinearGradientMode.cs new file mode 100644 index 0000000..8907963 --- /dev/null +++ b/src/Common/Drawing2D/LinearGradientMode.cs @@ -0,0 +1,27 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the direction of a linear gradient. +/// +public enum LinearGradientMode +{ + /// + /// Specifies a gradient from left to right. + /// + Horizontal = 0, + + /// + /// Specifies a gradient from top to bottom. + /// + Vertical = 1, + + /// + /// Specifies a gradient from upper left to lower right. + /// + ForwardDiagonal = 2, + + /// + /// Specifies a gradient from upper right to lower left. + /// + BackwardDiagonal = 3 +} From 693b07a90e3ae738ce793ffd47639c7d8fd95bef Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 20:32:52 -0300 Subject: [PATCH 010/217] add PathGradientBrush class --- src/Common/Drawing2D/PathGradientBrush.cs | 268 ++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 src/Common/Drawing2D/PathGradientBrush.cs diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs new file mode 100644 index 0000000..2522955 --- /dev/null +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -0,0 +1,268 @@ +using System; +using System.ComponentModel; +using System.Linq; +using SkiaSharp; + +namespace GeneXus.Drawing.Drawing2D; + +public sealed class PathGradientBrush : Brush +{ + private GraphicsPath m_path; + private WrapMode m_mode; + private Matrix m_transform; + private Blend m_blend; + private ColorBlend m_interpolation; + private PointF m_center, m_focus; + private Color m_color; + private Color[] m_surround; + + private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) + : base(new SKPaint { }) + { + m_path = path; + m_mode = mode; + + m_blend = null; + m_color = new Color(255, 255, 255, 255); + m_center = GetCentroid(m_path); + m_focus = new PointF(0, 0); + m_interpolation = new() + { + Colors = new[] { Color.Empty }, + Positions = new[] { 0f } + }; + m_surround = new[] { CenterColor }; + m_transform = transform; + + UpdateShader(() => { }); + } + + /// + /// Initializes a new instance of the class with the specified path. + /// + /// + public PathGradientBrush(GraphicsPath path) + : this(path, WrapMode.Clamp, new Matrix()) { } + + /// + /// Initializes a new instance of the class with the specified points and wrap mode. + /// + public PathGradientBrush(PointF[] points, WrapMode mode = WrapMode.Clamp) + : this(CreatePath(points), mode, new Matrix()) { } + + /// + /// Initializes a new instance of the class with the specified points and wrap mode. + /// + public PathGradientBrush(Point[] points, WrapMode mode = WrapMode.Clamp) + : this(Array.ConvertAll(points, point => new PointF(point.m_point)), mode) { } + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public override object Clone() + => m_path.Clone() is GraphicsPath path ? new PathGradientBrush(path, m_mode, m_transform) : throw new Exception("path could not be cloned."); + + #endregion + + + #region Properties + + /// + /// Gets or sets a that specifies positions and factors that define a + /// custom falloff for the gradient. + /// + public Blend Blend + { + get => m_blend; + set => UpdateShader(() => m_blend = value ?? throw new ArgumentNullException(nameof(value))); + } + + /// + /// Gets or sets the at the center of the path gradient. + /// + public Color CenterColor + { + get => m_color; + set => UpdateShader(() => m_color = value); + } + + /// + /// Gets or sets the center of the path gradient. + /// + public PointF CenterPoint + { + get => m_center; + set => UpdateShader(() => m_center = value); + } + + /// + /// Gets or sets the focus for the gradient falloff. + /// + public PointF FocusScales + { + get => m_focus; + set => UpdateShader(() => m_focus = value); + } + + /// + /// Gets or sets a that defines a multicolor linear gradient. + /// + public ColorBlend InterpolationColors + { + get => m_interpolation; + set => UpdateShader(() => m_interpolation = value); + } + + /// + /// Gets a bounding for this gradient. + /// + public RectangleF Rectangle => m_path.GetBounds(); + + /// + /// Gets or sets an array of structures that correspond to the + /// points in the path this gradient fills. + /// + public Color[] SurroundColors + { + get => m_surround; + set => UpdateShader(() => m_surround = value); + } + + /// + /// Gets or sets a copy of the that defines a local geometric + /// transform for this gradient. + /// + public Matrix Transform + { + get => m_transform; + set => UpdateShader(() => m_transform = value); + } + + /// + /// Gets or sets a that indicates the wrap mode for this gradient. + /// + public WrapMode WrapMode + { + get => m_mode; + set => UpdateShader(() => + { + if (value is < WrapMode.Tile or > WrapMode.Clamp) + throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(WrapMode)); + m_mode = value; + }); + } + + #endregion + + + #region Methods + + /// + /// Multiplies the object that represents the local + /// geometric transformation of this object + /// by the specified object in the specified order. + /// + public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend) + => UpdateShader(() => Transform.Multiply(matrix, order)); + + /// + /// Resets the Transform property of this object to identity. + /// + public void ResetTransform() + => UpdateShader(() => Transform.Reset()); + + /// + /// Rotates the local geometric transformation of this object + /// by the specified amount in the specified order. + /// + public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend) + => UpdateShader(() => Transform.Rotate(angle, order)); + + /// + /// Scales the local geometric transformation of this object + /// by the specified amounts in the specified order. + /// + public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend) + => UpdateShader(() => Transform.Scale(sx, sy, order)); + + /// + /// Creates a gradient with a center color and a linear falloff to each surrounding color. + /// + public void SetBlendTriangularShape(float focus, float scale = 1.0f) + => throw new NotImplementedException(); + + /// + /// Creates a gradient brush that changes color starting from the center of the path outward + /// to the path's boundary. The transition from one color to another is based on a bell-shaped curve. + /// + public void SetSigmaBellShape(float focus, float scale = 1.0f) + => throw new NotImplementedException(); + + /// + /// Translates the local geometric transformation of this object + /// by the specified dimensions in the specified order. + /// + public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrder.Prepend) + => UpdateShader(() => Transform.Translate(dx, dy, order)); + + #endregion + + + #region Utilities + + private static GraphicsPath CreatePath(PointF[] points) + { + var types = new byte[points.Length]; + types[0] = (byte)PathPointType.Start; + for (int i = 1; i < types.Length - 1; i++) + types[i] = (byte)PathPointType.Line; + types[types.Length - 1] = (byte)PathPointType.CloseSubpath; + return new GraphicsPath(points, types); + } + + private static PointF GetCentroid(GraphicsPath path) + { + var bounds = path.GetBounds(); + return new PointF(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2); + } + + private void UpdateShader(Action action) + { + action(); + + switch (m_mode) + { + case WrapMode.TileFlipX: + m_transform.Scale(-1, 1); + break; + case WrapMode.TileFlipY: + m_transform.Scale(1, -1); + break; + case WrapMode.TileFlipXY: + m_transform.Scale(-1, -1); + break; + } + + var center = m_center.m_point; + var focus = Math.Max(m_focus.X, m_focus.Y); + var factors = m_blend?.Factors ?? Enumerable.Repeat(1f, m_interpolation.Positions.Length).ToArray(); + var positions = m_interpolation.Positions + .Prepend(0f) + .Take(factors.Length) + .ToArray(); + var colors = m_interpolation.Colors + .Prepend(m_color) + .Zip(factors, ApplyFactor) + .Select(color => color.m_color) + .ToArray(); + var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; + var matrix = m_transform.m_matrix; + + m_paint.Shader = SKShader.CreateRadialGradient(center, focus, colors, positions, mode, matrix); + } + + #endregion +} From 457314d65fc5d048c5525a7f39294f704b14e21b Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 20:39:13 -0300 Subject: [PATCH 011/217] add Pen class and referenced enums --- src/Common/Drawing2D/DashCap.cs | 22 ++ src/Common/Drawing2D/LineCap.cs | 19 ++ src/Common/Drawing2D/LineJoin.cs | 29 +++ src/Common/Drawing2D/PenAlignment.cs | 32 +++ src/Common/Drawing2D/PenType.cs | 32 +++ src/Common/Pen.cs | 309 +++++++++++++++++++++++++++ 6 files changed, 443 insertions(+) create mode 100644 src/Common/Drawing2D/DashCap.cs create mode 100644 src/Common/Drawing2D/LineCap.cs create mode 100644 src/Common/Drawing2D/LineJoin.cs create mode 100644 src/Common/Drawing2D/PenAlignment.cs create mode 100644 src/Common/Drawing2D/PenType.cs create mode 100644 src/Common/Pen.cs diff --git a/src/Common/Drawing2D/DashCap.cs b/src/Common/Drawing2D/DashCap.cs new file mode 100644 index 0000000..0bc05d2 --- /dev/null +++ b/src/Common/Drawing2D/DashCap.cs @@ -0,0 +1,22 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the available dash cap styles with which a can end a line. +/// +public enum DashCap +{ + /// + /// Specifies a square cap that squares off both ends of each dash. + /// + Flat = 0, + + /// + /// Specifies a circular cap that rounds off both ends of each dash. + /// + Round = 2, + + /// + /// Specifies a triangular cap that points both ends of each dash. + /// + Triangle = 3 +} diff --git a/src/Common/Drawing2D/LineCap.cs b/src/Common/Drawing2D/LineCap.cs new file mode 100644 index 0000000..14c84c5 --- /dev/null +++ b/src/Common/Drawing2D/LineCap.cs @@ -0,0 +1,19 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the available cap styles with which a object can end a line. +/// +public enum LineCap +{ + Flat = 0, + Square = 1, + Round = 2, + Triangle = 3, + NoAnchor = 16, // corresponds to flat cap + SquareAnchor = 17, // corresponds to square cap + RoundAnchor = 18, // corresponds to round cap + DiamondAnchor = 19, // corresponds to triangle cap + ArrowAnchor = 20, // no correspondence + Custom = 255, // custom cap + AnchorMask = 40 // mask to check for anchor or not. +} diff --git a/src/Common/Drawing2D/LineJoin.cs b/src/Common/Drawing2D/LineJoin.cs new file mode 100644 index 0000000..24bf17a --- /dev/null +++ b/src/Common/Drawing2D/LineJoin.cs @@ -0,0 +1,29 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies how to join consecutive line or curve segments in a figure (subpath) contained in a object. +/// +public enum LineJoin +{ + /// + /// Specifies a mitered join. This produces a sharp corner or a clipped corner, depending on whether the + /// length of the miter exceeds the miter limit. + /// + Miter = 0, + + /// + /// Specifies a beveled join. This produces a diagonal corner. + /// + Bevel = 1, + + /// + /// pecifies a circular join. This produces a smooth, circular arc between the lines. + /// + Round = 2, + + /// + /// Specifies a mitered join. This produces a sharp corner or a beveled corner, depending on whether the + /// length of the miter exceeds the miter limit. + /// + MiterClipped = 3 +} diff --git a/src/Common/Drawing2D/PenAlignment.cs b/src/Common/Drawing2D/PenAlignment.cs new file mode 100644 index 0000000..9b9d78f --- /dev/null +++ b/src/Common/Drawing2D/PenAlignment.cs @@ -0,0 +1,32 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the alignment of a object in relation to the theoretical, zero-width line. +/// +public enum PenAlignment +{ + /// + /// Specifies the object is centered over the theoretical line. + /// + Center = 0, + + /// + /// Specifies the is positioned on the inside of the theoretical line. + /// + Inset = 1, + + /// + /// Specifies the is positioned on the outside of the theoretical line. + /// + Outset = 2, + + /// + /// Specifies the is positioned on the left of the theoretical line. + /// + Left = 3, + + /// + /// Specifies the is positioned on the right of the theoretical line. + /// + Right = 4 +} diff --git a/src/Common/Drawing2D/PenType.cs b/src/Common/Drawing2D/PenType.cs new file mode 100644 index 0000000..8562f79 --- /dev/null +++ b/src/Common/Drawing2D/PenType.cs @@ -0,0 +1,32 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the type of fill a object uses to fill lines. +/// +public enum PenType +{ + /// + /// Specifies a solid fill. + /// + SolidColor = 0, + + /// + /// Specifies a hatch fill. + /// + HatchFill = 1, + + /// + /// Specifies a bitmap texture fill. + /// + TextureFill = 2, + + /// + /// Specifies a path gradient fill. + /// + PathGradient = 3, + + /// + /// Specifies a linear gradient fill. + /// + LinearGradient = 4 +} diff --git a/src/Common/Pen.cs b/src/Common/Pen.cs new file mode 100644 index 0000000..bda55d2 --- /dev/null +++ b/src/Common/Pen.cs @@ -0,0 +1,309 @@ + +using System; +using SkiaSharp; +using GeneXus.Drawing.Drawing2D; + +namespace GeneXus.Drawing; + +public class Pen : ICloneable, IDisposable +{ + internal SKPaint m_paint; + private Brush m_brush = Brushes.Transparent; + + internal Pen(SKPaint paint, float width) + { + m_paint = paint ?? throw new ArgumentNullException(nameof(paint)); + m_paint.Style = SKPaintStyle.Stroke; + m_paint.StrokeWidth = width; + } + + /// + /// Initializes a new instance of the class with the + /// specified and . + /// + public Pen(Color color, float width = 1.0f) + : this (new SKPaint() { Color = color.m_color }, width) + { + m_brush = new SolidBrush(color); + } + + /// + /// Initializes a new instance of the class with the + /// specified and . + /// + public Pen(Brush brush, float width = 1.0f) + : this(brush.m_paint.Clone(), width) + { + m_brush = brush; + } + + /// + /// Cleans up resources for this . + /// + ~Pen() => Dispose(false); + + /// + /// Creates a human-readable string that represents this . + /// + public override string ToString() => $"{GetType().Name}: [{Color}, Width: {Width}]"; + + + #region IDisposable + + /// + /// Cleans up resources for this . + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + protected virtual void Dispose(bool disposing) => m_paint.Dispose(); + + #endregion + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public object Clone() + => new Pen(m_paint, m_paint.StrokeWidth); + + #endregion + + + #region Operators + + /// + /// Creates a with the coordinates of the specified . + /// + public static explicit operator SKPaint(Pen pen) => pen.m_paint; + + #endregion + + + #region Properties + + /// + /// Gets or sets the alignment for this . + /// + public PenAlignment Alignment + { + get => m_paint.TextAlign switch + { + SKTextAlign.Center => PenAlignment.Center, + SKTextAlign.Left => PenAlignment.Left, + SKTextAlign.Right => PenAlignment.Right, + _ => throw new NotSupportedException("skia mapping") + }; + set => m_paint.TextAlign = value switch + { + PenAlignment.Center => SKTextAlign.Center, + PenAlignment.Left => SKTextAlign.Left, + PenAlignment.Right => SKTextAlign.Right, + _ => throw new NotSupportedException("skia mapping") + }; + } + + /// + /// Gets or sets the that determines attributes of this . + /// + public Brush Brush + { + get => m_brush; + set + { + m_paint.Color = value.m_paint.Color; + m_brush = value; + } + } + + /// + /// Gets or sets the color of this . + /// + public Color Color + { + get => new(m_paint.Color); + set => m_paint.Color = value.m_color; + } + + /// + /// Gets or sets an array of values that specifies a compound . A compound + /// pen draws a compound line made up of parallel lines and spaces. + /// + public float[] CompoundArray + { + get => throw new NotImplementedException(); + set => m_paint.PathEffect = SKPathEffect.CreateDash(value, 0); + } + + /// + /// Gets or sets a custom cap to use at the end of lines drawn + /// with this . + /// + public object CustomEndCap // TODO: implement CustomLineCap class + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + /// + /// Gets or sets a custom cap to use at the beginning of lines drawn + /// with this . + /// + public object CustomStartCap // TODO: implement CustomLineCap class + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + /// + /// Gets or sets the cap style used at the end of the dashes that make + /// up dashed lines drawn with this . + /// + public DashCap DashCap + { + + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + /// + /// Gets or sets the distance from the start of a line to the beginning of a dash pattern. + /// + public float DashOffset + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + /// + /// Gets or sets an array of custom dashes and spaces. + /// + public float[] DashPattern + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + /// + /// Gets the style of lines drawn with this . + /// + public PenType PenType => m_brush switch + { + SolidBrush => PenType.SolidColor, + TextureBrush => PenType.TextureFill, + LinearGradientBrush => PenType.LinearGradient, + PathGradientBrush => PenType.PathGradient, + HatchBrush => PenType.HatchFill, + _ => throw new NotImplementedException() + }; + + /// + /// Gets or sets the cap style used at the beginning of lines drawn with this . + /// + public LineCap StartCap + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + /// + /// Gets or sets the cap style used at the end of lines drawn with this . + /// + public LineCap EndCap + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + /// + /// Gets or sets the join style for the ends of two consecutive lines drawn with this . + /// + public LineJoin LineJoin + { + get => m_paint.StrokeJoin switch + { + SKStrokeJoin.Bevel => LineJoin.Bevel, + SKStrokeJoin.Round => LineJoin.Round, + SKStrokeJoin.Miter => LineJoin.Miter, + _ => throw new NotImplementedException($"undefined map to {m_paint.StrokeJoin}.") + }; + set => m_paint.StrokeJoin = value switch + { + LineJoin.Bevel => SKStrokeJoin.Bevel, + LineJoin.Round => SKStrokeJoin.Round, + LineJoin.Miter => SKStrokeJoin.Miter, + _ => throw new ArgumentException($"undefined value {value}.", nameof(value)) + }; + } + + /// + /// Gets or sets the limit of the thickness of the join on a mitered corner. + /// + public float MiterLimit + { + get => m_paint.StrokeMiter; + set => m_paint.StrokeMiter = value; + } + + /// + /// Gets or sets a copy of the geometric transformation for this . + /// + public Matrix Transform { get; set; } = new Matrix(); + + /// + /// Gets or sets the width of this . + /// + public float Width + { + get => m_paint.StrokeWidth; + set => m_paint.StrokeWidth = value; + } + + #endregion + + + #region Mathod + + /// + /// Multiplies the transformation matrix for this by the specified . + /// + public void MultiplyTransform(Matrix matrix) + => Transform.Multiply(matrix); + + /// + /// Resets the geometric transformation matrix for this to identity. + /// + public void ResetTransform() + => Transform.Reset(); + + /// + /// Rotates the local geometric transformation by the specified angle. + /// + public void RotateTransform(float degrees, MatrixOrder order = MatrixOrder.Prepend) + => Transform.Rotate(degrees, order); + + /// + /// Scales the local geometric transformation by the specified factors. + /// + public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend) + => Transform.Scale(sx, sy, order); + + /// + /// Sets the values that determine the style of cap used to end lines drawn by this . + /// + public void SetLineCap(LineCap startCap, LineCap endCap, DashCap dashCap) + => throw new NotImplementedException(); + + /// + /// Translates the local geometric transformation by the specified dimensions. + /// + public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrder.Prepend) + => Transform.Translate(dx, dy, order); + + #endregion +} From 3c43ad06fff6263c9b776e153d37d98767e5a971 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 20:56:28 -0300 Subject: [PATCH 012/217] add GraphicsPath class methods' implementation depending on Pen and Brush --- src/Common/Drawing2D/GraphicsPath.cs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index dcb956b..478c48a 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -524,15 +524,20 @@ public RectangleF GetBounds() /// when this path is transformed by the specified . /// public RectangleF GetBounds(Matrix matrix) - => GetBounds(matrix, null); + => GetBounds(matrix, new Pen(Brushes.Transparent)); /// /// Returns a rectangle that bounds this /// when the current path is transformed by the specified and /// drawn with the specified . /// - public RectangleF GetBounds(Matrix matrix, object pen) - => throw new NotImplementedException(); + public RectangleF GetBounds(Matrix matrix, Pen pen) + { + var transformed = new GraphicsPath(m_path); + transformed.Transform(matrix); + var bounds = pen.m_paint.GetFillPath(transformed.m_path).Bounds; + return new RectangleF(bounds); + } /// /// Gets the last point in the array of this . @@ -545,7 +550,7 @@ public PointF GetLastPoint() /// this when drawn with the specified and /// using the specified . /// - public bool IsOutlineVisible(float x, float y, object pen, object g = null) + public bool IsOutlineVisible(float x, float y, Pen pen, object g = null) => IsOutlineVisible(new PointF(x, y), pen, g); /// @@ -553,15 +558,15 @@ public bool IsOutlineVisible(float x, float y, object pen, object g = null) /// within (under) the outline of this when drawn /// with the specified and using the specified . /// - public bool IsOutlineVisible(PointF point, object pen, object g = null) - => IsOutlineVisible(point.m_point, null, null); + public bool IsOutlineVisible(PointF point, Pen pen, object g = null) + => IsOutlineVisible(point.m_point, pen.m_paint, null); /// /// Indicates whether the specified point is contained within (under) the outline of /// this when drawn with the specified and /// using the specified . /// - public bool IsOutlineVisible(int x, int y, object pen, object g = null) + public bool IsOutlineVisible(int x, int y, Pen pen, object g = null) => IsOutlineVisible(new Point(x, y), pen, g); /// @@ -569,8 +574,8 @@ public bool IsOutlineVisible(int x, int y, object pen, object g = null) /// within (under) the outline of this when drawn /// with the specified and using the specified . /// - public bool IsOutlineVisible(Point point, object pen, object g = null) - => IsOutlineVisible(point.m_point, new SKPaint(), null); + public bool IsOutlineVisible(Point point, Pen pen, object g = null) + => IsOutlineVisible(point.m_point, pen.m_paint, null); /// /// Indicates whether the specified point's coordinate is contained @@ -649,7 +654,7 @@ public void Warp(PointF[] destPoints, RectangleF srcRect, Matrix matrix = null, /// filled when this path is drawn by the specified and flatness /// value for curves. /// - public void Widen(object pen, Matrix matrix = null, float flatness = 0.25f) + public void Widen(Pen pen, Matrix matrix = null, float flatness = 0.25f) => throw new NotImplementedException(); #endregion From 0e10e8e9ba89e1c4f62639b4c3ff39ad11f8e36c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 21:01:45 -0300 Subject: [PATCH 013/217] add Region class and RegionDataClass class, update RectangleF with required methods/properties --- src/Common/Drawing2D/RegionData.cs | 11 + src/Common/RectangleF.cs | 19 ++ src/Common/Region.cs | 472 +++++++++++++++++++++++++++++ 3 files changed, 502 insertions(+) create mode 100644 src/Common/Drawing2D/RegionData.cs create mode 100644 src/Common/Region.cs diff --git a/src/Common/Drawing2D/RegionData.cs b/src/Common/Drawing2D/RegionData.cs new file mode 100644 index 0000000..dd744bf --- /dev/null +++ b/src/Common/Drawing2D/RegionData.cs @@ -0,0 +1,11 @@ +namespace GeneXus.Drawing.Drawing2D; + +public sealed class RegionData +{ + internal RegionData(byte[] data) => Data = data; + + /// + /// Gets or sets an array of bytes that specify the object. + /// + public byte[] Data { get; set; } +} diff --git a/src/Common/RectangleF.cs b/src/Common/RectangleF.cs index 56c5ff2..f08db2a 100644 --- a/src/Common/RectangleF.cs +++ b/src/Common/RectangleF.cs @@ -46,6 +46,9 @@ public RectangleF(PointF location, float width, float height) #region Operators + /// + /// Converts the specified to a . + /// public static explicit operator SKRect(RectangleF rect) => rect.m_rect; /// @@ -190,6 +193,17 @@ public PointF Location /// public readonly bool IsEmpty => m_rect.IsEmpty; + /// + /// Gets a secuencie of that defines this . + /// + public readonly PointF[] Points => new[] + { + new PointF(Left, Top), + new PointF(Right, Top), + new PointF(Right, Bottom), + new PointF(Left, Bottom) + }; + #endregion @@ -286,5 +300,10 @@ public static RectangleF Inflate(RectangleF rect, float x, float y) /// public void Offset(PointF pos) => Offset(pos.X, pos.Y); + /// + /// Converts a by performing a round operation on all the coordinates. + /// + public static Rectangle Truncate(RectangleF rect) => new(unchecked((int)rect.X), unchecked((int)rect.Y), unchecked((int)rect.Width), unchecked((int)rect.Height)); + #endregion } diff --git a/src/Common/Region.cs b/src/Common/Region.cs new file mode 100644 index 0000000..0334239 --- /dev/null +++ b/src/Common/Region.cs @@ -0,0 +1,472 @@ +using System; +using SkiaSharp; +using GeneXus.Drawing.Drawing2D; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace GeneXus.Drawing; + +public sealed class Region : IDisposable +{ + internal readonly SKRegion m_region; + + private Region(SKRegion region) + { + m_region = region; + } + + private Region(SKRect rect) + : this(new SKRegion(SKRectI.Round(rect))) { } + + /// + /// Initializes a new . + /// + public Region() + : this(new SKRegion()) { } + + /// + /// Initializes a new from the specified structure. + /// + public Region(RectangleF rect) + : this(rect.m_rect) { } + + /// + /// Initializes a new from the specified structure. + /// + public Region(Rectangle rect) + : this(rect.m_rect) { } + + /// + /// Initializes a new from the specified structure. + /// + public Region(GraphicsPath path) + : this(new SKRegion(path.m_path)) { } + + /// + /// Initializes a new from the specified data. + /// + public Region(RegionData data) + : this(ParseRegionData(data)) { } + + /// + /// Cleans up resources for this . + /// + ~Region() => Dispose(false); + + + #region IDisposable + + /// + /// Releases all resources used by this . + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + private void Dispose(bool disposing) => m_region.Dispose(); + + #endregion + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public Region Clone() => new(new SKRegion(m_region.GetBoundaryPath())); + + #endregion + + + #region IEqualitable + + /// + /// Determines whether the specified object is equal to the current object. + /// + public override bool Equals(object obj) => obj is Region region && m_region.Equals(region.m_region); + + /// + /// Get the has code of this . + /// + public override int GetHashCode() => m_region.GetHashCode(); + + #endregion + + + #region Factory + + /// + /// Initializes a new from a handle to the specified existing GDI region. + /// + public static Region FromHrgn(IntPtr hrgn) + => throw new NotSupportedException("windows specific"); + + #endregion + + + #region Methods + + /// + /// Updates this to contain the portion of the specified that + /// does not intersect with this . + /// + public void Complement(Region region) + => m_region.Op(region.m_region, SKRegionOperation.ReverseDifference); + + /// + /// Updates this to contain the portion of the specified that + /// does not intersect with this . + /// + public void Complement(RectangleF rect) + => Complement(RectangleF.Truncate(rect)); + + /// + /// Updates this to contain the portion of the specified that + /// does not intersect with this . + /// + public void Complement(Rectangle rect) + => Complement(new Region(rect)); + + /// + /// Updates this to contain the portion of the specified that + /// does not intersect with this . + /// + public void Complement(GraphicsPath path) + => Complement(new Region(path)); + + /// + /// Updates this to contain only the portion of its interior that does not intersect + /// with the specified . + /// + public void Exclude(Region region) + => m_region.Op(region.m_region, SKRegionOperation.Difference); + + /// + /// Updates this to contain only the portion of its interior that does not intersect + /// with the specified . + /// + public void Exclude(RectangleF rect) + => Exclude(RectangleF.Truncate(rect)); + + /// + /// Updates this to contain only the portion of its interior that does not intersect + /// with the specified . + /// + public void Exclude(Rectangle rect) + => Exclude(new Region(rect)); + + /// + /// Updates this to contain only the portion of its interior that does not intersect + /// with the specified . + /// + public void Exclude(GraphicsPath path) + => Exclude(new Region(path)); + + /// + /// Gets a structure that represents a rectangle that bounds this + /// on the drawing surface of a object. + /// + public Rectangle GetBounds(object g) + => new(m_region.Bounds); // TODO: consider g + + /// + /// Returns a Windows handle to this in the specified graphics context. + /// + public IntPtr GetHrgn(object g) + => throw new NotSupportedException("windows specific"); + + /// + /// Returns a that represents the information that describes this . + /// + public RegionData GetRegionData() + { + using var iterator = m_region.CreateRectIterator(); + + var rects = new List(); + while (iterator.Next(out var rect)) + rects.Add(rect); + + var rdh = new RGNDATAHEADER + { + dwSize = (uint)Marshal.SizeOf(), + iType = 1, // RDH_RECTANGLES + nCount = (uint)rects.Count, + nRgnSize = (uint)(rects.Count * Marshal.SizeOf()) + }; + var headerSize = Marshal.SizeOf(); + var bufferSize = rects.Count * Marshal.SizeOf(); + var data = new byte[headerSize + bufferSize]; + + // Copy header to data + Buffer.BlockCopy(BitConverter.GetBytes(rdh.dwSize), 0, data, 0, 4); + Buffer.BlockCopy(BitConverter.GetBytes(rdh.iType), 0, data, 4, 4); + Buffer.BlockCopy(BitConverter.GetBytes(rdh.nCount), 0, data, 8, 4); + Buffer.BlockCopy(BitConverter.GetBytes(rdh.nRgnSize), 0, data, 12, 4); + + // Copy rectangles to data + for (int i = 0; i < rects.Count; i++) + { + var rectData = new byte[Marshal.SizeOf()]; + Buffer.BlockCopy(BitConverter.GetBytes(rects[i].Left), 0, rectData, 0, 4); + Buffer.BlockCopy(BitConverter.GetBytes(rects[i].Top), 0, rectData, 4, 4); + Buffer.BlockCopy(BitConverter.GetBytes(rects[i].Right), 0, rectData, 8, 4); + Buffer.BlockCopy(BitConverter.GetBytes(rects[i].Bottom), 0, rectData, 12, 4); + Buffer.BlockCopy(rectData, 0, data, headerSize + i * rectData.Length, rectData.Length); + } + + return new RegionData(data); + } + + /// + /// Returns an array of structures that approximate this after + /// the specified matrix transformation is applied. + /// + public Rectangle[] GetRegionScans(Matrix matrix) // TODO: apply Matrix + { + Transform(matrix); + using var iterator = m_region.CreateRectIterator(); + var rects = new List(); + while (iterator.Next(out var rect)) + rects.Add(new Rectangle(rect)); + return rects.ToArray(); + } + + /// + /// Updates this to the intersection of itself with the specified . + /// + public void Intersect(Region region) + => m_region.Op(region.m_region, SKRegionOperation.Intersect); + + /// + /// Updates this to the intersection of itself with the specified . + /// + public void Intersect(RectangleF rect) + => Intersect(RectangleF.Truncate(rect)); + + /// + /// Updates this to the intersection of itself with the specified . + /// + public void Intersect(Rectangle rect) + => Intersect(new Region(rect)); + + /// + /// Updates this to the intersection of itself with the specified . + /// + public void Intersect(GraphicsPath path) + => Intersect(new Region(path)); + + /// + /// Tests whether this has an empty interior on the specified drawing surface + /// of a object. + /// + public bool IsEmpty(object g) // TODO: consider g + => m_region.IsEmpty; + + /// + /// Tests whether this has an infinite interior on the specified drawing surface + /// of a object. + /// + public bool IsInfinite(object g) + => m_region.IsRect && m_region.Bounds.Width == int.MaxValue && m_region.Bounds.Width == int.MaxValue; + + /// + /// Tests whether any portion of the specified rectangle is contained within this when drawn + /// using the specified (if it is defined). + /// + public bool IsVisible(int x, int y, int width, int height, object g = null) + => IsVisible(new Rectangle(x, y, width, height), g); + + /// + /// Tests whether any portion of the specified rectangle is contained within this when drawn + /// using the specified (if it is defined). + /// + public bool IsVisible(float x, float y, float width, float height, object g = null) + => IsVisible(new RectangleF(x, y, width, height), g); + + /// + /// Tests whether any portion of the specified structure is contained within + /// this when drawn using the specified (if it is defined). + /// + public bool IsVisible(RectangleF rect, object g = null) + => IsVisible(RectangleF.Truncate(rect), g); + + /// + /// Tests whether any portion of the specified structure is contained within + /// this when drawn using the specified (if it is defined). + /// + public bool IsVisible(Rectangle rect, object g = null) + => m_region.Contains(SKRectI.Round(rect.m_rect)); + + /// + /// Tests whether the specified point is contained within this when drawn + /// using the specified (if it is defined). + /// + public bool IsVisible(int x, int y, object g = null) + => IsVisible(new Point(x, y), g); + + /// + /// Tests whether the specified point is contained within this when drawn + /// using the specified (if it is defined). + /// + public bool IsVisible(float x, float y, object g = null) + => IsVisible(new PointF(x, y), g); + + /// + /// Tests whether the specified structure is contained within + /// this when drawn using the specified (if it is defined). + /// + public bool IsVisible(PointF point, object g = null) + => IsVisible(PointF.Truncate(point), g); + + /// + /// Tests whether the specified structure is contained within + /// this when drawn using the specified (if it is defined). + /// + public bool IsVisible(Point point, object g = null) + => m_region.Contains(SKPointI.Round(point.m_point)); + + /// + /// Initializes this to an empty interior. + /// + public void MakeEmpty() + => m_region.SetEmpty(); + + /// + /// Initializes this to an empty interior. + /// + public void MakeInfinite() + => m_region.SetRect(SKRectI.Create(int.MaxValue, int.MaxValue)); + + /// + /// Releases the handle of the . + /// + public void ReleaseHrgn(IntPtr regionHandle) + => throw new NotImplementedException(); + + /// + /// Transforms this by the specified . + /// + public void Transform(Matrix matrix) + { + var path = m_region.GetBoundaryPath(); + path.Transform(matrix.m_matrix); + m_region.SetPath(path); + } + + /// + /// Offsets the coordinates of this by the specified amount. + /// + public void Translate(float dx, float dy) + => m_region.Translate((int)Math.Round(dx), (int)Math.Round(dy)); + + /// + /// Updates this to the union of itself with the specified . + /// + public void Union(Region region) + => m_region.Op(region.m_region, SKRegionOperation.Union); + + /// + /// Updates this to the union of itself with the specified . + /// + public void Union(RectangleF rect) + => Union(RectangleF.Truncate(rect)); + + /// + /// Updates this to the union of itself with the specified . + /// + public void Union(Rectangle rect) + => Union(new Region(rect)); + + /// + /// Updates this to the union of itself with the specified . + /// + public void Union(GraphicsPath path) + => Union(new Region(path)); + + /// + /// Updates this to the union minus the intersection of itself with the specified . + /// + public void Xor(Region region) + => m_region.Op(region.m_region, SKRegionOperation.XOR); + + /// + /// Updates this to the union minus the intersection of itself with the specified . + /// + public void Xor(RectangleF rect) + => Xor(RectangleF.Truncate(rect)); + + /// + /// Updates this to the union minus the intersection of itself with the specified . + /// + public void Xor(Rectangle rect) + => Xor(new Region(rect)); + + /// + /// Updates this to the union minus the intersection of itself with the specified . + /// + public void Xor(GraphicsPath path) + => Xor(new Region(path)); + + #endregion + + + #region Utilities + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + private struct RGNDATAHEADER + { + public uint dwSize; + public uint iType; + public uint nCount; + public uint nRgnSize; + } + + private static SKRegion ParseRegionData(RegionData data) + { + var rgnData = data.Data; + var headerSize = Marshal.SizeOf(); + var rectSize = Marshal.SizeOf(); + + if (rgnData.Length < headerSize) + throw new ArgumentException("invalid region data."); + + var rdh = new RGNDATAHEADER + { + dwSize = BitConverter.ToUInt32(rgnData, 0), + iType = BitConverter.ToUInt32(rgnData, 4), + nCount = BitConverter.ToUInt32(rgnData, 8), + nRgnSize = BitConverter.ToUInt32(rgnData, 12) + }; + + if (rgnData.Length < headerSize + rdh.nRgnSize) + throw new ArgumentException("invalid region data."); + + var region = new SKRegion(); + for (int i = 0; i < rdh.nCount; i++) + { + var offset = headerSize + i * rectSize; + var rect = new SKRectI + { + Left = BitConverter.ToInt32(rgnData, offset + 0), + Top = BitConverter.ToInt32(rgnData, offset + 4), + Right = BitConverter.ToInt32(rgnData, offset + 8), + Bottom = BitConverter.ToInt32(rgnData, offset + 12) + }; + region.Op(rect, SKRegionOperation.Union); + } + return region; + } + + #endregion +} From 0b960068870a837f937652ec2faaaf515cb25795 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 22:05:30 -0300 Subject: [PATCH 014/217] add Graphics and referenced enum/class, update references from other classes --- src/Common/Bitmap.cs | 4 +- src/Common/CopyPixelOperation.cs | 97 ++ src/Common/Drawing2D/CombineMode.cs | 39 + src/Common/Drawing2D/CompositeMode.cs | 18 + src/Common/Drawing2D/CompositingQuality.cs | 37 + src/Common/Drawing2D/CoordinateSpace.cs | 26 + src/Common/Drawing2D/FlushIntention.cs | 18 + src/Common/Drawing2D/GraphicsContainer.cs | 7 + src/Common/Drawing2D/GraphicsPath.cs | 28 +- src/Common/Drawing2D/InterpolationMode.cs | 56 + src/Common/Drawing2D/PixelOffsetMode.cs | 38 + src/Common/Drawing2D/SmoothingMode.cs | 37 + src/Common/Font.cs | 8 +- src/Common/Graphics.cs | 1781 ++++++++++++++++++++ src/Common/Rectangle.cs | 19 + src/Common/Region.cs | 34 +- src/Common/Text/TextRenderingHint.cs | 37 + 17 files changed, 2247 insertions(+), 37 deletions(-) create mode 100644 src/Common/CopyPixelOperation.cs create mode 100644 src/Common/Drawing2D/CombineMode.cs create mode 100644 src/Common/Drawing2D/CompositeMode.cs create mode 100644 src/Common/Drawing2D/CompositingQuality.cs create mode 100644 src/Common/Drawing2D/CoordinateSpace.cs create mode 100644 src/Common/Drawing2D/FlushIntention.cs create mode 100644 src/Common/Drawing2D/GraphicsContainer.cs create mode 100644 src/Common/Drawing2D/InterpolationMode.cs create mode 100644 src/Common/Drawing2D/PixelOffsetMode.cs create mode 100644 src/Common/Drawing2D/SmoothingMode.cs create mode 100644 src/Common/Graphics.cs create mode 100644 src/Common/Text/TextRenderingHint.cs diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 756e8d7..0993b3e 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -53,8 +53,8 @@ public Bitmap(float width, float height) /// Initializes a new instance of the class with the specified size /// and with the resolution of the specified object. /// - //public Bitmap(int width, int height, object g) // TODO: Implement Graphics - // : this(width, height) => throw new NotImplementedException(); + public Bitmap(int width, int height, Graphics g) // TODO: Implement Graphics + : this(width, height) => throw new NotImplementedException(); /// /// Initializes a new instance of the class with the specified size and format. diff --git a/src/Common/CopyPixelOperation.cs b/src/Common/CopyPixelOperation.cs new file mode 100644 index 0000000..3b98b89 --- /dev/null +++ b/src/Common/CopyPixelOperation.cs @@ -0,0 +1,97 @@ +namespace GeneXus.Drawing; + +/// +/// Specifies the Copy Pixel (ROP) operation. +/// +public enum CopyPixelOperation +{ + /// + /// Fills the Destination Rectangle using the color associated with the index 0 in the physical palette. + /// (This color is black for the default physical palette.) + /// + Blackness = 0x00000042, + + /// + /// Includes any windows that are layered on top. + /// + CaptureBlt = 0x40000000, + + /// + /// The destination area is inverted. + /// + DestinationInvert = 0x00550009, + + /// + /// The colors of the source area are merged with the colors of the selected brush of the destination device + /// context using the Boolean AND operator. + /// + MergeCopy = 0x00C000CA, + + /// + /// The colors of the inverted source area are merged with the colors of the destination area by using the Boolean OR operator. + /// + MergePaint = 0x00BB0226, + + /// + /// The bitmap is not mirrored. + /// + NoMirrorBitmap = unchecked((int)0x80000000), + + /// + /// The inverted source area is copied to the destination. + /// + NotSourceCopy = 0x00330008, + + /// + /// The source and destination colors are combined using the Boolean OR operator, and then the resultant color is inverted. + /// + NotSourceErase = 0x001100A6, + + /// + /// The brush currently selected in the destination device context is copied to the destination bitmap. + /// + PatCopy = 0x00F00021, + + /// + /// The colors of the brush currently selected in the destination device context are combined with the colors of + /// the destination area using the Boolean XOR operator. + /// + PatInvert = 0x005A0049, + + /// + /// The colors of the brush currently selected in the destination device context are combined with the colors of + /// the inverted source area using the Boolean OR operator. The result of this operation is combined with the colors of the destination area using the Boolean OR operator. + /// + PatPaint = 0x00FB0A09, + + /// + /// The colors of the source and destination areas are combined using the Boolean AND operator. + /// + SourceAnd = 0x008800C6, + + /// + /// The source area is copied directly to the destination area. + /// + SourceCopy = 0x00CC0020, + + /// + /// The inverted colors of the destination area are combined with the colors of the source area using the Boolean AND operator. + /// + SourceErase = 0x00440328, + + /// + /// The colors of the source and destination areas are combined using the Boolean XOR operator. + /// + SourceInvert = 0x00660046, + + /// + /// The colors of the source and destination areas are combined using the Boolean OR operator. + /// + SourcePaint = 0x00EE0086, + + /// + /// Fills the destination area using the color associated with index 1 in the physical palette. + /// (This color is white for the default physical palette.) + /// + Whiteness = 0x00FF0062, +} \ No newline at end of file diff --git a/src/Common/Drawing2D/CombineMode.cs b/src/Common/Drawing2D/CombineMode.cs new file mode 100644 index 0000000..e7f2120 --- /dev/null +++ b/src/Common/Drawing2D/CombineMode.cs @@ -0,0 +1,39 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies how different clipping regions can be combined. +/// +public enum CombineMode +{ + /// + /// One clipping region is replaced by another. + /// + Replace = 0, + + /// + /// Two clipping regions are combined by taking their intersection. + /// + Intersect = 1, + + /// + /// Two clipping regions are combined by taking the union of both. + /// + Union = 2, + + /// + /// Two clipping regions are combined by taking only the areas enclosed by one or the other region, but not both. + /// + Xor = 3, + + /// + /// Specifies that the existing region is replaced by the result of the new region being removed from the existing + /// region. Said differently, the new region is excluded from the existing region. + /// + Exclude = 4, + + /// + /// Specifies that the existing region is replaced by the result of the existing region being removed from the new + /// region. Said differently, the existing region is excluded from the new region. + /// + Complement = 5 +} diff --git a/src/Common/Drawing2D/CompositeMode.cs b/src/Common/Drawing2D/CompositeMode.cs new file mode 100644 index 0000000..b71746e --- /dev/null +++ b/src/Common/Drawing2D/CompositeMode.cs @@ -0,0 +1,18 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies how the source colors are combined with the background colors. +/// +public enum CompositeMode +{ + /// + /// Specifies that when a color is rendered, it overwrites the background color. + /// + SourceOver = 0, + + /// + /// Specifies that when a color is rendered, it is blended with the background + /// color. The blend is determined by the alpha component of the color being rendered. + /// + SourceCopy = 1 +} diff --git a/src/Common/Drawing2D/CompositingQuality.cs b/src/Common/Drawing2D/CompositingQuality.cs new file mode 100644 index 0000000..7785397 --- /dev/null +++ b/src/Common/Drawing2D/CompositingQuality.cs @@ -0,0 +1,37 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies how the source colors are combined with the background colors. +/// +public enum CompositingQuality +{ + /// + /// Invalid quality. + /// + Invalid = -1, + + /// + /// Default quality. + /// + Default = 0, + + /// + /// High speed, low quality. + /// + HighSpeed = 1, + + /// + /// High quality, low speed compositing. + /// + HighQuality = 2, + + /// + /// Gamma correction is used. + /// + GammaCorrected = 3, + + /// + /// Assume linear values. + /// + AssumeLinear = 4 +} diff --git a/src/Common/Drawing2D/CoordinateSpace.cs b/src/Common/Drawing2D/CoordinateSpace.cs new file mode 100644 index 0000000..c855c0b --- /dev/null +++ b/src/Common/Drawing2D/CoordinateSpace.cs @@ -0,0 +1,26 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the system to use when evaluating coordinates. +/// +public enum CoordinateSpace +{ + /// + /// Specifies that coordinates are in the world coordinate context. World coordinates are + /// used in a nonphysical environment, such as a modeling environment. + /// + World = 0, + + /// + /// Specifies that coordinates are in the page coordinate context. Their units are defined + /// by the property, and must be one of the elements of + /// the enumeration. + /// + Page = 1, + + /// + /// Specifies that coordinates are in the device coordinate context. On a computer screen + /// the device coordinates are usually measured in pixels. + /// + Device = 2 +} diff --git a/src/Common/Drawing2D/FlushIntention.cs b/src/Common/Drawing2D/FlushIntention.cs new file mode 100644 index 0000000..99536cc --- /dev/null +++ b/src/Common/Drawing2D/FlushIntention.cs @@ -0,0 +1,18 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies whether commands in the graphics stack are terminated (flushed) immediately +/// or executed as soon as possible. +/// +public enum FlushIntention +{ + /// + /// Flush all batched rendering operations. + /// + Flush = 0, + + /// + /// Flush all batched rendering operations and wait for them to complete. + /// + Sync = 1 +} diff --git a/src/Common/Drawing2D/GraphicsContainer.cs b/src/Common/Drawing2D/GraphicsContainer.cs new file mode 100644 index 0000000..e30e5ad --- /dev/null +++ b/src/Common/Drawing2D/GraphicsContainer.cs @@ -0,0 +1,7 @@ +namespace GeneXus.Drawing.Drawing2D; + +public sealed class GraphicsContainer +{ + internal int m_state = -1; + internal GraphicsContainer(int state) => m_state = state; +} diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 478c48a..88a0203 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -322,8 +322,8 @@ public void AddEllipse(float x, float y, float width, float height) => AddEllipse(new RectangleF(x, y, width, height)); /// - /// Adds an ellipse to the current path bounded by - /// a structure. + /// Adds an ellipse to the current path bounded by + /// a structure. /// public void AddEllipse(RectangleF rect) => AddEllipse(rect.m_rect); @@ -550,7 +550,7 @@ public PointF GetLastPoint() /// this when drawn with the specified and /// using the specified . /// - public bool IsOutlineVisible(float x, float y, Pen pen, object g = null) + public bool IsOutlineVisible(float x, float y, Pen pen, Graphics g = null) => IsOutlineVisible(new PointF(x, y), pen, g); /// @@ -558,15 +558,15 @@ public bool IsOutlineVisible(float x, float y, Pen pen, object g = null) /// within (under) the outline of this when drawn /// with the specified and using the specified . /// - public bool IsOutlineVisible(PointF point, Pen pen, object g = null) - => IsOutlineVisible(point.m_point, pen.m_paint, null); + public bool IsOutlineVisible(PointF point, Pen pen, Graphics g = null) + => IsOutlineVisible(point.m_point, pen.m_paint, g?.m_canvas.LocalClipBounds); /// /// Indicates whether the specified point is contained within (under) the outline of /// this when drawn with the specified and /// using the specified . /// - public bool IsOutlineVisible(int x, int y, Pen pen, object g = null) + public bool IsOutlineVisible(int x, int y, Pen pen, Graphics g = null) => IsOutlineVisible(new Point(x, y), pen, g); /// @@ -574,36 +574,36 @@ public bool IsOutlineVisible(int x, int y, Pen pen, object g = null) /// within (under) the outline of this when drawn /// with the specified and using the specified . /// - public bool IsOutlineVisible(Point point, Pen pen, object g = null) - => IsOutlineVisible(point.m_point, pen.m_paint, null); + public bool IsOutlineVisible(Point point, Pen pen, Graphics g = null) + => IsOutlineVisible(point.m_point, pen.m_paint, g?.m_canvas.LocalClipBounds); /// /// Indicates whether the specified point's coordinate is contained /// within this . /// - public bool IsVisible(float x, float y, object g = null) + public bool IsVisible(float x, float y, Graphics g = null) => IsVisible(new PointF(x, y), g); /// /// Indicates whether the specified structure is /// contained within this . /// - public bool IsVisible(PointF point, object g = null) // TODO: consider Graphics - => IsVisible(point.m_point, null); + public bool IsVisible(PointF point, Graphics g = null) + => IsVisible(point.m_point, g?.m_canvas.LocalClipBounds); /// /// Indicates whether the specified point's coordinate is contained /// within this . /// - public bool IsVisible(int x, int y, object g = null) + public bool IsVisible(int x, int y, Graphics g = null) => IsVisible(new Point(x, y), g); /// /// Indicates whether the specified structure is /// contained within this . /// - public bool IsVisible(Point point, object g = null) // TODO: consider Graphics - => IsVisible(point.m_point, null); + public bool IsVisible(Point point, Graphics g = null) // TODO: consider Graphics + => IsVisible(point.m_point, g?.m_canvas.LocalClipBounds); /// /// Empties the and arrays diff --git a/src/Common/Drawing2D/InterpolationMode.cs b/src/Common/Drawing2D/InterpolationMode.cs new file mode 100644 index 0000000..22a9444 --- /dev/null +++ b/src/Common/Drawing2D/InterpolationMode.cs @@ -0,0 +1,56 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies the algorithm that is used when images are scaled or rotated. +/// +public enum InterpolationMode +{ + /// + /// Specifies an invalid mode. + /// + Invalid = -1, + + /// + /// Specifies default mode. + /// + Default = 0, + + /// + /// Specifies low quality interpolation. + /// + Low = 1, + + /// + /// Specifies high quality interpolation. + /// + High = 2, + + /// + /// Specifies bilinear interpolation. No prefiltering is done. This mode is not + /// suitable for shrinking an image below 50 percent of its original size. + /// + Bilinear = 3, + + /// + /// Specifies bicubic interpolation. No prefiltering is done. This mode is not + /// suitable for shrinking an image below 25 percent of its original size. + /// + Bicubic = 4, + + /// + /// Specifies nearest-neighbor interpolation. + /// + NearestNeighbor = 5, + + /// + /// Specifies high-quality, bilinear interpolation. Prefiltering is performed to + /// ensure high-quality shrinking. + /// + HighQualityBilinear = 6, + + /// + /// Specifies high-quality, bicubic interpolation. Prefiltering is performed to + /// ensure high-quality shrinking. This mode produces the highest quality transformed images. + /// + HighQualityBicubic = 7 +} diff --git a/src/Common/Drawing2D/PixelOffsetMode.cs b/src/Common/Drawing2D/PixelOffsetMode.cs new file mode 100644 index 0000000..c1cdf07 --- /dev/null +++ b/src/Common/Drawing2D/PixelOffsetMode.cs @@ -0,0 +1,38 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies how pixels are offset during rendering. +/// +public enum PixelOffsetMode +{ + /// + /// Specifies an invalid mode. + /// + Invalid = -1, + + /// + /// Specifies the default mode. + /// + Default = 0, + + /// + /// Specifies high speed, low quality rendering. + /// + HighSpeed = 1, + + /// + /// Specifies high quality, low speed rendering. + /// + HighQuality = 2, + + /// + /// Specifies no pixel offset. + /// + None = 3, + + /// + /// Specifies that pixels are offset by -0.5 units, both horizontally + /// and vertically, for high speed antialiasing. + /// + Half = 4 +} diff --git a/src/Common/Drawing2D/SmoothingMode.cs b/src/Common/Drawing2D/SmoothingMode.cs new file mode 100644 index 0000000..a923776 --- /dev/null +++ b/src/Common/Drawing2D/SmoothingMode.cs @@ -0,0 +1,37 @@ +namespace GeneXus.Drawing.Drawing2D; + +/// +/// Specifies whether smoothing (antialiasing) is applied to lines and curves and the edges of filled areas. +/// +public enum SmoothingMode +{ + /// + /// Specifies an invalid mode. + /// + Invalid = -1, + + /// + /// Specifies no antialiasing. + /// + Default = 0, + + /// + /// Specifies no antialiasing. + /// + HighSpeed = 1, + + /// + /// Specifies antialiased rendering. + /// + HighQuality = 2, + + /// + /// Specifies no antialiasing. + /// + None = 3, + + /// + /// Specifies antialiased rendering. + /// + AntiAlias = 4 +} diff --git a/src/Common/Font.cs b/src/Common/Font.cs index 7b36ab0..b323db5 100644 --- a/src/Common/Font.cs +++ b/src/Common/Font.cs @@ -325,8 +325,8 @@ public float GetHeight() /// /// Returns the line spacing, in the current unit of a specified Graphics, of this . /// - public float GetHeight(object graphics) - => throw new NotImplementedException(); // TODO: will be replaced by GetHeight(graphics.PageUnit, graphics.DpiY) when Graphics class had been implemented + public float GetHeight(Graphics graphics) + => GetHeight(graphics.PageUnit, graphics.DpiY); /// /// Returns the height, in pixels, of this when drawn to a device with the specified vertical resolution. @@ -391,7 +391,7 @@ public void ToLogFont(out Interop.LOGFONT logFont) => ToLogFont(out logFont, null); /// - public void ToLogFont(out Interop.LOGFONT logFont, object graphics) + public void ToLogFont(out Interop.LOGFONT logFont, Graphics graphics) => ToLogFont(logFont = new Interop.LOGFONT(), graphics); /// @@ -406,7 +406,7 @@ public void ToLogFont(object logFont) /// /// An to represent the structure that this method creates. /// A Graphics that provides additional information for the structure. - public void ToLogFont(object logFont, object graphics) + public void ToLogFont(object logFont, Graphics graphics) => throw new NotSupportedException("unsupported by skia."); #endregion diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs new file mode 100644 index 0000000..5d06d24 --- /dev/null +++ b/src/Common/Graphics.cs @@ -0,0 +1,1781 @@ +using System; +using System.Numerics; +using SkiaSharp; +using GeneXus.Drawing.Drawing2D; +using GeneXus.Drawing.Text; +using System.Linq; +using System.Collections.Generic; + +namespace GeneXus.Drawing; + +public sealed class Graphics : IDisposable +{ + internal readonly SKCanvas m_canvas; + internal readonly SKBitmap m_bitmap; + + private Graphics(SKBitmap bitmap) + { + m_bitmap = bitmap; + m_canvas = new SKCanvas(m_bitmap); + + VisibleClipBounds = new RectangleF(m_canvas.LocalClipBounds); + Clip = new Region(VisibleClipBounds); + } + + /// + /// Cleans up resources for this . + /// + ~Graphics() => Dispose(false); + + + #region IDisposable + + /// + /// Cleans up resources for this . + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + using var surface = SKSurface.Create(m_bitmap.Info); + surface.Draw(m_canvas, 0, 0, new SKPaint() { Color = SKColors.Transparent }); + surface.Canvas.ClipRegion(Clip.m_region); + + using var image = surface.Snapshot(); + using var bitmap = SKBitmap.FromImage(image); + m_bitmap.SetPixels(bitmap.GetPixels()); + } + m_canvas.Dispose(); + } + + #endregion + + + #region Operations + + /// + /// Creates a with the coordinates of the specified . + /// + public static explicit operator SKCanvas(Graphics graphic) => graphic.m_canvas; + + #endregion + + + #region Properties + + /// + /// Gets or sets a that limits the drawing region of this . + /// + public Region Clip { get; set; } + + /// + /// Gets a structure that bounds the clipping region of this . + /// + public RectangleF ClipBounds => new(m_canvas.LocalClipBounds); + + /// + /// Gets a value that specifies how composited images are drawn to this . + /// + public CompositeMode CompositingMode { get; set; } = CompositeMode.SourceOver; + + /// + /// Gets or sets the rendering quality of composited images drawn to this . + /// + public CompositingQuality CompositingQuality { get; set; } = CompositingQuality.Default; + + /// + /// Gets the horizontal resolution of this . + /// + public float DpiX => (int)(100 * m_canvas.DeviceClipBounds.Width / m_canvas.LocalClipBounds.Width); + + /// + /// Gets the vertical resolution of this . + /// + public float DpiY => (int)(100 * m_canvas.DeviceClipBounds.Height / m_canvas.LocalClipBounds.Height); + + /// + /// Gets or sets the interpolation mode associated with this . + /// + public InterpolationMode InterpolationMode { get; set; } = InterpolationMode.Bilinear; + + /// + /// Gets a value indicating whether the clipping region of this is empty. + /// + public bool IsClipEmpty => m_canvas.IsClipEmpty; + + /// + /// Gets a value indicating whether the visible clipping region of this is empty. + /// + public bool IsVisibleClipEmpty => m_canvas.LocalClipBounds is { Width: <= 0, Height: <= 0 }; + + /// + /// Gets or sets the scaling between world units and page units for this . + /// + public float PageScale { get; set; } = 1; + + /// + /// Gets or sets the unit of measure used for page coordinates in this . + /// + public GraphicsUnit PageUnit { get; set; } = GraphicsUnit.Display; + + /// + /// Gets or sets a value specifying how pixels are offset during rendering of this . + /// + public PixelOffsetMode PixelOffsetMode { get; set; } = PixelOffsetMode.Default; + + /// + /// Gets or sets the rendering origin of this for dithering and for hatch brushes. + /// + public Point RenderingOrigin { get; set; } = new Point(0, 0); + + /// + /// Gets or sets the rendering quality for this . + /// + public SmoothingMode SmoothingMode { get; set; } = SmoothingMode.None; + + /// + /// Gets or sets the gamma correction value for rendering text. + /// + public int TextContrast { get; set; } = 4; + + /// + /// Gets or sets the rendering mode for text associated with this . + /// + public TextRenderingHint TextRenderingHint { get; set; } = TextRenderingHint.SystemDefault; + + /// + /// Gets or sets a copy of the geometric world transformation for this . + /// + public Matrix Transform { get; set; } = new Matrix(); + + /// + /// Gets or sets the world transform elements for this . + /// + public Matrix3x2 TransformElements + { + get => Transform.MatrixElements; + set => Transform.MatrixElements = value; + } + + /// + /// Gets the bounding rectangle of the visible clipping region of this . + /// + public RectangleF VisibleClipBounds { get; set; } + + #endregion + + + #region Factory + + /// + /// Creates a new from the specified handle to a device + /// context. + /// + public static Graphics FromHdc(IntPtr hdc) + => FromHdc(hdc, IntPtr.Zero); + + /// + /// Creates a new from the specified handle to a device + /// context and handle to a device. + /// + public static Graphics FromHdc(IntPtr hdc, IntPtr hdevice) + => throw new NotImplementedException(); + + /// + /// Returns a for the specified device context. + /// + public static Graphics FromHdcInternal(IntPtr hdc) + => throw new NotImplementedException(); + + /// + /// Creates a new from the specified handle to a window. + /// + public static Graphics FromHwnd(IntPtr hwnd) + => throw new NotImplementedException(); + + /// + /// Creates a new for the specified windows handle. + /// + public static Graphics FromHwndInternal(IntPtr hwnd) + => throw new NotImplementedException(); + + /// + /// Creates a new from the specified . + /// + public static Graphics FromImage(Image image) + { + var bitmap = image is Bitmap bm ? bm.m_bitmap : SKBitmap.FromImage(image.InnerImage); + var graphics = new Graphics(bitmap); + graphics.DrawImage(image, 0, 0); + return graphics; + } + + #endregion + + + #region Methods + + /// + /// Adds a comment to the current . + /// + public void AddMetafileComment(byte[] data) + => throw new NotImplementedException(); + + /// + /// Saves a graphics container with the current state of this and + /// and opens and uses a new graphics container. + /// + public GraphicsContainer BeginContainer() + => BeginContainer(new RectangleF(0, 0, 1, 1), new RectangleF(0, 0, 1, 1), GraphicsUnit.Pixel); + + /// + /// Saves a graphics container with the current state of this and + /// opens and uses a new graphics container with the specified scale transformation. + /// + public GraphicsContainer BeginContainer(RectangleF dstRect, RectangleF srcRect, GraphicsUnit unit) + { + int state = m_canvas.Save(); + + float factorX = GetUnitFactor(DpiX, unit); + float factorY = GetUnitFactor(DpiY, unit); + + var src = RectangleF.Inflate(srcRect, factorX, factorY); + var dst = RectangleF.Inflate(dstRect, factorX, factorY); + + float scaleX = dst.Width / src.Width; + float scaleY = dst.Height / src.Height; + + float translateX = dst.Left - src.Left * scaleX; + float translateY = dst.Top - src.Top * scaleY; + + m_canvas.Translate(translateX, translateY); + m_canvas.Scale(scaleX, scaleY); + + return new GraphicsContainer(state); + } + + /// + /// Saves a graphics container with the current state of this and + /// opens and uses a new graphics container with the specified scale transformation. + /// + public GraphicsContainer BeginContainer(Rectangle dstRect, Rectangle srcRect, GraphicsUnit unit) + => BeginContainer(new RectangleF(dstRect.m_rect), new RectangleF(srcRect.m_rect), unit); + + /// + /// Clears the entire drawing surface and fills it with a transparent background color. + /// + public void Clear() + => Clear(Color.Transparent); + + /// + /// Clears the entire drawing surface and fills it with the specified background color. + /// + public void Clear(Color color) + => m_canvas.Clear(color.m_color); + + /// + /// Performs a bit-block transfer of the color data, corresponding to a rectangle of pixels, + /// from the screen to the drawing surface of the . + /// + public void CopyFromScreen(Point srcUpperLeft, Point dstUpperLeft, Size blockRegionSize, CopyPixelOperation copyPixelOperation = CopyPixelOperation.SourceCopy) + => CopyFromScreen(srcUpperLeft.X, srcUpperLeft.Y, dstUpperLeft.X, dstUpperLeft.Y, blockRegionSize, copyPixelOperation); + + /// + /// Performs a bit-block transfer of the color data, corresponding to a rectangle of pixels, + /// from the screen to the drawing surface of the . + /// + public void CopyFromScreen(int srcX, int srcY, int dstX, int dstY, Size blockRegionSize, CopyPixelOperation copyPixelOperation = CopyPixelOperation.SourceCopy) + => throw new NotSupportedException(); + + /// + /// Draws an arc representing a portion of an ellipse specified by a structure. + /// + public void DrawArc(Pen pen, RectangleF oval, float startAngle, float sweepAngle) + => m_canvas.DrawArc(oval.m_rect, startAngle, sweepAngle, false, pen.m_paint); + + /// + /// Draws an arc representing a portion of an ellipse specified by a pair of coordinates, a width, and a height. + /// + public void DrawArc(Pen pen, float x, float y, float width, float height, float startAngule, float sweepAngle) + => DrawArc(pen, new RectangleF(x, y, width, height), startAngule, sweepAngle); + + /// + /// Draws an arc representing a portion of an ellipse specified by a structure. + /// + public void DrawArc(Pen pen, Rectangle oval, float startAngle, float sweepAngle) + => DrawArc(pen, new RectangleF(oval.m_rect), startAngle, sweepAngle); + + /// + /// Draws an arc representing a portion of an ellipse specified by a pair of coordinates, a width, and a height. + /// + public void DrawArc(Pen pen, int x, int y, int width, int height, float startAngule, float sweepAngle) + => DrawArc(pen, new Rectangle(x, y, width, height), startAngule, sweepAngle); + + /// + /// Draws a cubic Bezier curve defined by four points. + /// + public void DrawBezier(Pen pen, PointF pt1, PointF pt2, PointF pt3, PointF pt4) + => DrawBeziers(pen, new[] { pt1, pt2, pt3, pt4 }); + + /// + /// Draws a cubic Bezier curve defined by four ordered pairs that represent points. + /// + public void DrawBezier(Pen pen, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) + => DrawBezier(pen, new PointF(x1, y1), new PointF(x2, y2), new PointF(x3, y3), new PointF(x4, y4)); + + /// + /// Draws a cubic Bezier curve defined by four points. + /// + public void DrawBezier(Pen pen, Point pt1, Point pt2, Point pt3, Point pt4) + => DrawBeziers(pen, new[] { pt1, pt2, pt3, pt4 }); + + /// + /// Draws a cubic Bezier curve defined by four ordered pairs that represent points. + /// + public void DrawBezier(Pen pen, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) + => DrawBezier(pen, new Point(x1, y1), new Point(x2, y2), new Point(x3, y3), new Point(x4, y4)); + + /// + /// Draws a series of Bézier splines from an array of structures. + /// + public void DrawBeziers(Pen pen, params PointF[] points) + { + if (points.Length < 4 || points.Length % 3 != 1) + throw new ArgumentException("invalid number of points for drawing Bezier curves", nameof(points)); + + using var path = new SKPath(); + path.MoveTo(points[0].m_point); + for (int i = 1; i < points.Length - 2; i += 3) + path.CubicTo(points[i].m_point, points[i + 1].m_point, points[i + 2].m_point); + + m_canvas.DrawPath(path, pen.m_paint); + } + + /// + /// Draws a series of Bézier splines from an array of structures. + /// + public void DrawBeziers(Pen pen, params Point[] points) + => DrawBeziers(pen, Array.ConvertAll(points, point => new PointF(point.m_point))); + + /// + /// Draws the given . + /// + public void DrawCachedBitmap(object cachedBitmap, int x, int y) + => throw new NotImplementedException(); + + /// + /// Draws a closed cardinal spline defined by an array of structures using a specified tension + /// + public void DrawClosedCurve(Pen pen, PointF[] points, float tension = 0.5f, FillMode fillMode = FillMode.Winding) + { + using var path = GetClosedCurvePath(points, fillMode, tension); + m_canvas.DrawPath(path.m_path, pen.m_paint); + } + + /// + /// Draws a closed cardinal spline defined by an array of structures using a specified tension + /// + public void DrawClosedCurve(Pen pen, Point[] points, float tension = 0.5f, FillMode fillMode = FillMode.Winding) + => DrawClosedCurve(pen, Array.ConvertAll(points, point => new PointF(point.m_point)), tension, fillMode); + + /// + /// Draws a cardinal spline through a specified array of structures using a specified tension. The drawing + /// begins offset from the beginning of the array. + /// + public void DrawCurve(Pen pen, PointF[] points, int offset, int numberOfSegments, float tension = 0.5f) + { + // TODO: include tension + if (offset < 0 || offset >= points.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + + if (numberOfSegments < 1 || offset + numberOfSegments >= points.Length) + throw new ArgumentOutOfRangeException(nameof(numberOfSegments)); + + using var path = new SKPath(); + path.MoveTo(points[offset].m_point); + for (int i = offset + 1; i < offset + numberOfSegments - 1; i++) + { + PointF p0 = points[Math.Max(i - 1, 0)]; + PointF p1 = points[i + 0]; + PointF p2 = points[i + 1]; + PointF p3 = points[Math.Min(i + 2, points.Length - 1)]; + + // calculate the tangents + float t1x = (p2.X - p0.X) * tension; + float t1y = (p2.Y - p0.Y) * tension; + float t2x = (p3.X - p1.X) * tension; + float t2y = (p3.Y - p1.Y) * tension; + + // calculate the control points + var p1x = points[i + 0].X + t1x / 3; + var p1y = points[i + 0].Y + t1y / 3; + var p2x = points[i + 1].X - t2x / 3; + var p2y = points[i + 1].Y - t2y / 3; + + // add the cubic Bezier segment to the path + path.CubicTo(p1x, p1y, p2x, p2y, points[i + 1].X, points[i + 1].Y); + } + + m_canvas.DrawPath(path, pen.m_paint); + } + + /// + /// Draws a cardinal spline through a specified array of structures using a specified tension. The drawing + /// begins offset from the beginning of the array. + /// + public void DrawCurve(Pen pen, Point[] points, int offset, int numberOfSegments, float tension = 0.5f) + => DrawCurve(pen, Array.ConvertAll(points, point => new PointF(point.m_point)), offset, numberOfSegments, tension); + + /// + /// Draws a cardinal spline through a specified array of structures using a specified tension. + /// + public void DrawCurve(Pen pen, PointF[] points, float tension = 0.5f) + => DrawCurve(pen, points, 0, points.Length, tension); + + /// + /// Draws a cardinal spline through a specified array of structures using a specified tension. + /// + public void DrawCurve(Pen pen, Point[] points, float tension = 0.5f) + => DrawCurve(pen, Array.ConvertAll(points, point => new PointF(point.m_point)), 0, points.Length, tension); + + /// + /// Draws an ellipse defined by a bounding rectangle specified by coordinates for the upper-left corner of the + /// rectangle, a height, and a width. + /// + public void DrawEllipse(Pen pen, float x, float y, float width, float height) + => DrawEllipse(pen, new RectangleF(x, y, width, height)); + + /// + /// Draws an ellipse specified by a bounding structure. + /// + public void DrawEllipse(Pen pen, RectangleF rect) + => m_canvas.DrawOval(rect.m_rect, pen.m_paint); + + /// + /// Draws an ellipse defined by a bounding rectangle specified by coordinates for the upper-left corner of the + /// rectangle, a height, and a width. + /// + public void DrawEllipse(Pen pen, int x, int y, int width, int height) + => DrawEllipse(pen, new Rectangle(x, y, width, height)); + + /// + /// Draws an ellipse specified by a bounding structure. + /// + public void DrawEllipse(Pen pen, Rectangle rect) + => DrawEllipse(pen, new RectangleF(rect.m_rect)); + + /// + /// Draws the image represented by the specified within the area specified + /// by a structure. + /// + public void DrawIcon(Icon icon, Rectangle rect) + => DrawImage(icon.ToBitmap(), rect); + + /// + /// Draws the image represented by the specified at the specified coordinates. + /// + public void DrawIcon(Icon icon, int x, int y) + => DrawImage(icon.ToBitmap(), x, y); + + /// + /// Draws the image represented by the specified without scaling the image. + /// + public void DrawIconUnstretched(Icon icon, Rectangle rect) + => DrawImageUnscaled(icon.ToBitmap(), rect); + + /// + /// Draws the specified , using its original physical size, at the specified + /// location given by a structure. + /// + public void DrawImage(Image image, PointF point) + => DrawImage(image, point.X, point.Y); + + /// + /// Draws the specified , using its original physical size, at the specified location. + /// + public void DrawImage(Image image, float x, float y) + => DrawImage(image, x, y, image.Width, image.Height); + + /// + /// Draws the specified , using its original physical size, at the specified + /// location given by a structure. + /// + public void DrawImage(Image image, Point point) + => DrawImage(image, new PointF(point.m_point)); + + /// + /// Draws the specified , using its original physical size, at the specified location. + /// + public void DrawImage(Image image, int x, int y) + => DrawImage(image, x, y, image.Width, image.Height); + + /// + /// Draws the specified at the specified location and with the specified + /// size given by a structure. + /// + public void DrawImage(Image image, RectangleF rect) + => m_canvas.DrawImage(image.InnerImage, rect.m_rect); + + /// + /// Draws the specified at the specified location and with the specified size. + /// + public void DrawImage(Image image, float x, float y, float width, float height) + => DrawImage(image, new RectangleF(x, y, width, height)); + + /// + /// Draws the specified at the specified location and with the specified + /// size given by a structure. + /// + public void DrawImage(Image image, Rectangle rect) + => DrawImage(image, new RectangleF(rect.m_rect)); + + /// + /// Draws the specified at the specified location and with the specified size. + /// + public void DrawImage(Image image, int x, int y, int width, int height) + => DrawImage(image, new Rectangle(x, y, width, height)); + + /// + /// Draws the specified at the specified location and with the specified + /// shape and size. + /// + public void DrawImage(Image image, PointF[] points) + { + var unit = GraphicsUnit.Pixel; + var bounds = image.GetBounds(ref unit); + DrawImage(image, points, new RectangleF(bounds.m_rect), unit); + } + + /// + /// Draws the specified at the specified location and with the specified + /// shape and size. + /// + public void DrawImage(Image image, Point[] points) + => DrawImage(image, Array.ConvertAll(points, point => new PointF(point.m_point))); + + /// + /// Draws a portion of an at a specified location. + /// + public void DrawImage(Image image, float x, float y, RectangleF rect, GraphicsUnit unit) + => DrawImage(image, rect, new RectangleF(x, y, image.Width, image.Height), unit); + + /// + /// Draws a portion of an at a specified location. + /// + public void DrawImage(Image image, int x, int y, Rectangle rect, GraphicsUnit unit) + => DrawImage(image, rect, new RectangleF(x, y, image.Width, image.Height), unit); + + /// + /// Draws the specified portion of the specified at the specified location + /// and with the specified size. + /// + public void DrawImage(Image image, RectangleF destination, RectangleF source, GraphicsUnit unit) + { + float factorX = GetUnitFactor(DpiX, unit); + float factorY = GetUnitFactor(DpiY, unit); + + var src = RectangleF.Inflate(source, factorX, factorY); + var dst = RectangleF.Inflate(destination, factorX, factorY); + + m_canvas.DrawImage(image.InnerImage, src.m_rect, dst.m_rect); + } + + /// + /// Draws the specified portion of the specified at the specified location + /// and with the specified size. + /// + public void DrawImage(Image image, Rectangle destination, Rectangle source, GraphicsUnit unit) + => DrawImage(image, new RectangleF(destination.m_rect), new RectangleF(source.m_rect), unit); + + /// + /// Draws a portion of an at a specified location and with the specified size. + /// + public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object imageAtt = null, object callback = null, int callbackData = 0) + { + // TODO: Implement ImageAttributes (attributes), DrawImageAbort (callback) and IntPtr (callbackData) + var path = new SKPath(); + path.MoveTo(destPoints[0].m_point); + for (int i = 1; i < destPoints.Length; i++) + path.LineTo(destPoints[i].m_point); + path.Close(); + + float factorX = GetUnitFactor(DpiX, srcUnit); + float factorY = GetUnitFactor(DpiY, srcUnit); + + using var surface = SKSurface.Create(image.InnerImage.Info); + surface.Canvas.DrawImage(image.InnerImage, 0, 0); + surface.Canvas.ClipPath(path); + surface.Canvas.ClipRect(srcRect.m_rect); + surface.Canvas.Scale(factorX, factorY); + + m_canvas.DrawSurface(surface, 0, 0); + } + + /// + /// Draws a portion of an at a specified location and with the specified size. + /// + public void DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, object imageAtt = null, object callback = null, int callbackData = 0) + => DrawImage(image, Array.ConvertAll(destPoints, point => new PointF(point.m_point)), new RectangleF(srcRect.m_rect), srcUnit, imageAtt, callback, callbackData); + + /// + /// Draws the specified portion of the specified at the specified location and with the specified size. + /// + public void DrawImage(Image image, RectangleF destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, object imageAttrs = null, object callback = null) + => DrawImage(image, destRect, srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttrs, callback, IntPtr.Zero); + + /// + /// Draws the specified portion of the specified at the specified location and with the specified size. + /// + public void DrawImage(Image image, RectangleF destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, object imageAttrs, object callback, IntPtr callbackData) + { + var destPoints = new PointF[] + { + new(destRect.Left, destRect.Top), + new(destRect.Right, destRect.Top), + new(destRect.Right, destRect.Bottom), + new(destRect.Left, destRect.Bottom), + }; + DrawImage(image, destPoints, new RectangleF(srcX, srcY, srcWidth, srcHeight), srcUnit, imageAttrs, callback, unchecked((int)callbackData)); + } + + /// + /// Draws the specified portion of the specified at the specified location and with the specified size. + /// + public void DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, object imageAttrs = null, object callback = null) + => DrawImage(image, destRect, srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttrs, callback, IntPtr.Zero); + + /// + /// Draws the specified portion of the specified at the specified location and with the specified size. + /// + public void DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, object imageAttrs, object callback, IntPtr callbackData) + => DrawImage(image, new RectangleF(destRect.m_rect), srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttrs, callback, callbackData); + + /// + /// Draws the specified using its original physical size at a specified location. + /// + public void DrawImageUnscaled(Image image, Point point) + => DrawImageUnscaled(image, point.X, point.Y); + + /// + /// Draws the specified using its original physical size at the location specified by a coordinate pair. + /// + public void DrawImageUnscaled(Image image, int x, int y) + => DrawImageUnscaled(image, x, y, image.Width, image.Height); + + /// + /// Draws the specified using its original physical size at a specified location. + /// + public void DrawImageUnscaled(Image image, Rectangle rect) + => DrawImage(image, rect); + + /// + /// Draws the specified using its original physical size at a specified location. + /// + public void DrawImageUnscaled(Image image, int x, int y, int width, int height) + => DrawImageUnscaled(image, new Rectangle(x, y, width, height)); + + /// + /// Draws the specified without scaling and clips it, if necessary, to fit in the specified rectangle. + /// + public void DrawImageUnscaledAndClipped(Image image, Rectangle rect) + => DrawImage(image, rect, rect, GraphicsUnit.Pixel); + + /// + /// Draws a line connecting the two points specified by the coordinate pairs. + /// + public void DrawLine(Pen pen, float x1, float y1, float x2, float y2) + => DrawLine(pen, new PointF(x1, y1), new PointF(x2, y2)); + + /// + /// Draws a line connecting two structures. + /// + public void DrawLine(Pen pen, PointF point1, PointF point2) + => m_canvas.DrawLine(point1.m_point, point2.m_point, pen.m_paint); + + /// + /// Draws a line connecting the two points specified by the coordinate pairs. + /// + public void DrawLine(Pen pen, int x1, int y1, int x2, int y2) + => DrawLine(pen, new Point(x1, y1), new Point(x2, y2)); + + /// + /// Draws a line connecting two structures. + /// + public void DrawLine(Pen pen, Point point1, Point point2) + => DrawLine(pen, new PointF(point1.m_point), new PointF(point2.m_point)); + + /// + /// Draws a series of line segments that connect an array of structures. + /// + public void DrawLines(Pen pen, params PointF[] points) + { + if (points.Length < 2) + throw new ArgumentException("must define at least 2 points", nameof(points)); + for (int i = 1; i < points.Length; i++) + DrawLine(pen, points[i - 1], points[i]); + } + + /// + /// Draws a series of line segments that connect an array of structures. + /// + public void DrawLines(Pen pen, params Point[] points) + => DrawLines(pen, Array.ConvertAll(points, point => new PointF(point.m_point))); + + /// + /// Draws a . + /// + public void DrawPath(Pen pen, GraphicsPath path) + => m_canvas.DrawPath(path.m_path, pen.m_paint); + + /// + /// Draws a pie shape defined by an ellipse specified by a structure and two radial lines. + /// + public void DrawPie(Pen pen, RectangleF rect, float startAngle, float sweepAngle) + { + using var path = GetPiePath(rect, startAngle, sweepAngle); + m_canvas.DrawPath(path.m_path, pen.m_paint); + } + + /// + /// Draws a pie shape defined by an ellipse specified by a structure and two radial lines. + /// + public void DrawPie(Pen pen, Rectangle rect, float startAngle, float sweepAngle) + => DrawPie(pen, new RectangleF(rect.m_rect), startAngle, sweepAngle); + + /// + /// Draws a pie shape defined by an ellipse specified by a coordinate pair, a width, a height, and two radial lines. + /// + public void DrawPie(Pen pen, float x, float y, float width, float height, float startAngle, float sweepAngle) + => DrawPie(pen, new RectangleF(x, y, width, height), startAngle, sweepAngle); + + /// + /// Draws a pie shape defined by an ellipse specified by a coordinate pair, a width, a height, and two radial lines. + /// + public void DrawPie(Pen pen, int x, int y, int width, int height, float startAngle, float sweepAngle) + => DrawPie(pen, new Rectangle(x, y, width, height), startAngle, sweepAngle); + + /// + /// Draws a polygon defined by an array of structures. + /// + public void DrawPolygon(Pen pen, params PointF[] points) + { + using var path = GetPolygonPath(points, FillMode.Winding); + m_canvas.DrawPath(path.m_path, pen.m_paint); + } + + /// + /// Draws a polygon defined by an array of structures. + /// + public void DrawPolygon(Pen pen, params Point[] points) + => DrawPolygon(pen, Array.ConvertAll(points, point => new PointF(point.m_point))); + + /// + /// Draws a rectangle specified by a structure. + /// + public void DrawRectangle(Pen pen, RectangleF rect) + => m_canvas.DrawRect(rect.m_rect, pen.m_paint); + + /// + /// Draws a rectangle specified by a structure. + /// + public void DrawRectangle(Pen pen, Rectangle rect) + => DrawRectangle(pen, new RectangleF(rect.m_rect)); + + /// + /// Draws a rectangle specified by a coordinate pair, a width, and a height. + /// + public void DrawRectangle(Pen pen, float x, float y, float witdth, float height) + => DrawRectangle(pen, new RectangleF(x, y, witdth, height)); + + /// + /// Draws a rectangle specified by a coordinate pair, a width, and a height. + /// + public void DrawRectangle(Pen pen, int x, int y, int witdth, int height) + => DrawRectangle(pen, new Rectangle(x, y, witdth, height)); + + /// + /// Draws a series of rectangles specified by structures. + /// + public void DrawRectangles(Pen pen, RectangleF[] rects) + => Array.ForEach(rects, rect => DrawRectangle(pen, rect)); + + /// + /// Draws a series of rectangles specified by structures. + /// + public void DrawRectangles(Pen pen, Rectangle[] rects) + => DrawRectangles(pen, Array.ConvertAll(rects, rect => new RectangleF(rect.m_rect))); + + /// + /// Draws the specified text at the specified location with the specified and + /// objects using the formatting attributes of the specified StringFormat. + /// + public void DrawString(ReadOnlySpan text, Font font, Brush brush, float x, float y, StringFormat format = null) + => DrawString(new string(text.ToArray()), font, brush, x, y, format); + + /// + /// Draws the specified text at the specified location with the specified and + /// objects using the formatting attributes of the specified StringFormat. + /// + public void DrawString(string text, Font font, Brush brush, float x, float y, StringFormat format = null) + => DrawString(text, font, brush, new PointF(x, y), format); + + /// + /// Draws the specified text string in the specified location with the specified and + /// objects using the formatting attributes of the specified StringFormat. + /// text, Font font, Brush brush, PointF point, StringFormat format = null) + => DrawString(new string(text.ToArray()), font, brush, new RectangleF(point.X, point.Y, 0, 0), format); + + /// + /// Draws the specified text string in the specified location with the specified and + /// objects using the formatting attributes of the specified StringFormat. + /// DrawString(text, font, brush, new RectangleF(point.X, point.Y, 0, 0), format); + + /// + /// Draws the specified text string in the specified rectangle with the specified and + /// objects using the formatting attributes of the specified StringFormat. + /// + public void DrawString(ReadOnlySpan text, Font font, Brush brush, RectangleF layout, StringFormat format = null) + => DrawString(new string(text.ToArray()), font, brush, layout, format); + + /// + /// Draws the specified text string in the specified rectangle with the specified and + /// objects using the formatting attributes of the specified StringFormat. + /// + public void DrawString(string text, Font font, Brush brush, RectangleF layout, StringFormat format = null) + { + using var path = new GraphicsPath(); + path.AddString(text, font.FontFamily, (int)font.Style, font.Size, layout, format); + m_canvas.DrawPath(path.m_path, brush.m_paint); + } + + /// + /// Closes the current graphics container and restores the state of this to + /// the state saved by a call to the method. + /// + public void EndContainer(GraphicsContainer container) + => m_canvas.RestoreToCount(container.m_state); + + /// + /// Sends the records in the specified Metafile, one at a time, to a callback method + /// for display at a specified point. + /// + public void EnumerateMetafile(object metafile, PointF destPoint, object callback) + => EnumerateMetafile(metafile, destPoint, callback, IntPtr.Zero); + + /// + /// Sends the records in the specified Metafile, one at a time, to a callback method + /// for display at a specified point. + /// + public void EnumerateMetafile(object metafile, PointF destPoint, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destPoint, callback, callbackData, null); + + /// + /// Sends the records in the specified Metafile, one at a time, to a callback method + /// for display at a specified point. + /// + public void EnumerateMetafile(object metafile, Point destPoint, object callback) + => EnumerateMetafile(metafile, destPoint, callback, IntPtr.Zero); + + /// + /// Sends the records in the specified Metafile, one at a time, to a callback method + /// for display at a specified point. + /// + public void EnumerateMetafile(object metafile, Point destPoint, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destPoint, callback, callbackData, null); + + /// + /// Sends the records of the specified Metafile, one at a time, to a callback method + /// for display in a specified rectangle. + /// + public void EnumerateMetafile(object metafile, RectangleF destRect, object callback) + => EnumerateMetafile(metafile, destRect, callback, IntPtr.Zero); + + /// + /// Sends the records of the specified Metafile, one at a time, to a callback method + /// for display in a specified rectangle. + /// + public void EnumerateMetafile(object metafile, RectangleF destRect, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destRect, callback, callbackData, null); + + /// + /// Sends the records of the specified Metafile, one at a time, to a callback method + /// for display in a specified rectangle. + /// + public void EnumerateMetafile(object metafile, Rectangle destRect, object callback) + => EnumerateMetafile(metafile, destRect, callback, IntPtr.Zero); + + /// + /// Sends the records of the specified Metafile, one at a time, to a callback method + /// for display in a specified rectangle. + /// + public void EnumerateMetafile(object metafile, Rectangle destRect, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destRect, callback, callbackData, null); + + /// + /// Sends the records in the specified Metafile, one at a time, to a callback method + /// for display in a specified parallelogram. + /// + public void EnumerateMetafile(object metafile, PointF[] destPoints, object callback) + => EnumerateMetafile(metafile, destPoints, callback, IntPtr.Zero); + + /// + /// Sends the records in the specified Metafile, one at a time, to a callback method + /// for display in a specified parallelogram. + /// + public void EnumerateMetafile(object metafile, PointF[] destPoints, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destPoints, callback, callbackData, null); + + /// + /// Sends the records in the specified Metafile, one at a time, to a callback method + /// for display in a specified parallelogram. + /// + public void EnumerateMetafile(object metafile, Point[] destPoints, object callback) + => EnumerateMetafile(metafile, destPoints, callback, IntPtr.Zero); + + /// + /// Sends the records in the specified Metafile, one at a time, to a callback method + /// for display in a specified parallelogram. + /// + public void EnumerateMetafile(object metafile, Point[] destPoints, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destPoints, callback, callbackData, null); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display at a specified point. + /// + public void EnumerateMetafile(object metafile, PointF destPoint, RectangleF srcRect, GraphicsUnit srcUnit, object callback) + => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, IntPtr.Zero); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display at a specified point. + /// + public void EnumerateMetafile(object metafile, PointF destPoint, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, callbackData, null); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display at a specified point. + /// + public void EnumerateMetafile(object metafile, Point destPoint, Rectangle srcRect, GraphicsUnit srcUnit, object callback) + => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, IntPtr.Zero); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display at a specified point. + /// + public void EnumerateMetafile(object metafile, Point destPoint, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, callbackData, null); + + /// + /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified rectangle. + /// + public void EnumerateMetafile(object metafile, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit, object callback) + => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, IntPtr.Zero); + + /// + /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified rectangle. + /// + public void EnumerateMetafile(object metafile, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, callbackData, null); + + /// + /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified rectangle. + /// + public void EnumerateMetafile(object metafile, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit, object callback) + => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, IntPtr.Zero); + + /// + /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified rectangle. + /// + public void EnumerateMetafile(object metafile, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, callbackData, null); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified parallelogram. + /// + public void EnumerateMetafile(object metafile, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object callback) + => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, IntPtr.Zero); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified parallelogram. + /// + public void EnumerateMetafile(object metafile, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, callbackData, null); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified parallelogram. + /// + public void EnumerateMetafile(object metafile, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, object callback) + => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, IntPtr.Zero); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified parallelogram. + /// + public void EnumerateMetafile(object metafile, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData) + => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, callbackData, null); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display at a specified point using specified image attributes. + /// + public void EnumerateMetafile(object metafile, PointF destPoint, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, new[] { destPoint }, callback, callbackData, imageAttr); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display at a specified point using specified image attributes. + /// + public void EnumerateMetafile(object metafile, Point destPoint, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, new[] { destPoint }, callback, callbackData, imageAttr); + + /// + /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified rectangle using specified image attributes. + /// + public void EnumerateMetafile(object metafile, RectangleF destRect, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, destRect.Points, callback, callbackData, imageAttr); + + /// + /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified rectangle using specified image attributes. + /// + public void EnumerateMetafile(object metafile, Rectangle destRect, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, destRect.Points, callback, callbackData, imageAttr); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified parallelogram using specified image attributes. + /// + public void EnumerateMetafile(object metafile, PointF[] destPoints, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, destPoints, RectangleF.Empty, GraphicsUnit.Pixel, callback, callbackData, imageAttr); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified parallelogram using specified image attributes. + /// + public void EnumerateMetafile(object metafile, Point[] destPoints, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, destPoints, Rectangle.Empty, GraphicsUnit.Pixel, callback, callbackData, imageAttr); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display at a specified point using specified image attributes. + /// + public void EnumerateMetafile(object metafile, PointF destPoint, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, new[] { destPoint }, srcRect, srcUnit, callback, callbackData, imageAttr); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display at a specified point using specified image attributes. + /// + public void EnumerateMetafile(object metafile, Point destPoint, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, new[] { destPoint }, srcRect, srcUnit, callback, callbackData, imageAttr); + + /// + /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified rectangle using specified image attributes. + /// + public void EnumerateMetafile(object metafile, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, destRect.Points, srcRect, srcUnit, callback, callbackData, imageAttr); + + /// + /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified rectangle using specified image attributes. + /// + public void EnumerateMetafile(object metafile, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, destRect.Points, srcRect, srcUnit, callback, callbackData, imageAttr); + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified parallelogram using specified image attributes. + /// + public void EnumerateMetafile(object metafile, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr) + => throw new NotImplementedException(); // TODO: Implement Metafile, EnumerateMetafileProc, ImageAttributes classes + + /// + /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback + /// method for display in a specified parallelogram using specified image attributes. + /// + public void EnumerateMetafile(object metafile, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr) + => EnumerateMetafile(metafile, Array.ConvertAll(destPoints, point => new PointF(point.m_point)), new RectangleF(srcRect.m_rect), srcUnit, callback, callbackData, imageAttr); + + /// + /// Updates the clip region of this to exclude the area specified + /// by a . + /// + public void ExcludeClip(Region region) + => SetClip(region, CombineMode.Exclude); + + /// + /// Updates the clip region of this to exclude the area specified + /// by a structure. + /// + public void ExcludeClip(Rectangle rect) + => ExcludeClip(new Region(rect)); + + /// + /// Fills the interior of a closed cardinal spline curve defined by an array + /// of structures using the specified fill mode and tension + /// + public void FillClosedCurve(Brush brush, PointF[] points, FillMode fillMode = FillMode.Alternate, float tension = 0.5f) + { + using var path = GetClosedCurvePath(points, fillMode, tension); + m_canvas.DrawPath(path.m_path, brush.m_paint); + } + + /// + /// Fills the interior of a closed cardinal spline curve defined by an array + /// of structures using the specified fill mode and tension + /// + public void FillClosedCurve(Brush brush, Point[] points, FillMode fillMode = FillMode.Alternate, float tension = 0.5f) + => FillClosedCurve(brush, Array.ConvertAll(points, point => new PointF(point.m_point))); + + /// + /// Fills the interior of an ellipse defined by a bounding rectangle specified + /// by a pair of coordinates, a width, and a height. + /// + public void FillEllipse(Brush brush, float x, float y, float width, float height) + => FillEllipse(brush, new RectangleF(x, y, width, height)); + + /// + /// Fills the interior of an ellipse defined by a bounding rectangle specified + /// by a pair of coordinates, a width, and a height. + /// + public void FillEllipse(Brush brush, int x, int y, int width, int height) + => FillEllipse(brush, new Rectangle(x, y, width, height)); + + /// + /// Fills the interior of an ellipse defined by a bounding + /// specified by a Rectangle structure. + /// + public void FillEllipse(Brush brush, RectangleF rect) + => m_canvas.DrawOval(rect.m_rect, brush.m_paint); + + /// + /// Fills the interior of an ellipse defined by a bounding + /// specified by a Rectangle structure. + /// + public void FillEllipse(Brush brush, Rectangle rect) + => FillEllipse(brush, new RectangleF(rect.m_rect)); + + /// + /// Fills the interior of a . + /// + public void FillPath(Brush brush, GraphicsPath path) + => m_canvas.DrawPath(path.m_path, brush.m_paint); + + /// + /// Fills the interior of a pie section defined by an ellipse specified by + /// a structure and two radial lines. + /// + public void FillPie(Brush brush, RectangleF oval, float startAngle, float sweepAngle) + { + using var path = GetPiePath(oval, startAngle, sweepAngle); + m_canvas.DrawPath(path.m_path, brush.m_paint); + } + + /// + /// Fills the interior of a pie section defined by an ellipse specified by + /// a structure and two radial lines. + /// + public void FillPie(Brush brush, Rectangle oval, float startAngle, float sweepAngle) + => FillPie(brush, new RectangleF(oval.m_rect), startAngle, sweepAngle); + + /// + /// Fills the interior of a pie section defined by an ellipse specified by a + /// pair of coordinates, a width, a height, and two radial lines. + /// + public void FillPie(Brush brush, float x, float y, float width, float height, float startAngle, float sweepAngle) + => FillPie(brush, new RectangleF(x, y, width, height), startAngle, sweepAngle); + + /// + /// Fills the interior of a pie section defined by an ellipse specified by a + /// pair of coordinates, a width, a height, and two radial lines. + /// + public void FillPie(Brush brush, int x, int y, int width, int height, float startAngle, float sweepAngle) + => FillPie(brush, new Rectangle(x, y, width, height), startAngle, sweepAngle); + + /// + /// Fills the interior of a polygon defined by an array of structures + /// and optionally using the specified fill mode. + /// + public void FillPolygon(Brush brush, PointF[] points, FillMode fillMode = FillMode.Alternate) + { + using var path = GetPolygonPath(points, fillMode); + m_canvas.DrawPath(path.m_path, brush.m_paint); + } + + /// + /// Fills the interior of a polygon defined by an array of structures + /// and optionally using the specified fill mode. + /// + public void FillPolygon(Brush brush, Point[] points, FillMode fillMode = FillMode.Alternate) + => FillPolygon(brush, Array.ConvertAll(points, point => new PointF(point.m_point)), fillMode); + + /// + /// Fills the interior of a rectangle specified by a structure. + /// + public void FillRectangle(Brush brush, RectangleF rect) + => m_canvas.DrawRect(rect.m_rect, brush.m_paint); + + /// + /// Fills the interior of a rectangle specified by a structure. + /// + public void FillRectangle(Brush brush, Rectangle rect) + => FillRectangle(brush, new RectangleF(rect.m_rect)); + + /// + /// Fills the interior of a rectangle specified by a pair of coordinates, a width, and a height. + /// + public void FillRectangle(Brush brush, float x, float y, float width, float height) + => FillRectangle(brush, new RectangleF(x, y, width, height)); + + /// + /// Fills the interior of a rectangle specified by a pair of coordinates, a width, and a height. + /// + public void FillRectangle(Brush brush, int x, int y, int width, int height) + => FillRectangle(brush, new Rectangle(x, y, width, height)); + + /// + /// Fills the interiors of a series of rectangles specified by structures. + /// + public void FillRectangles(Brush brush, params RectangleF[] rects) + => Array.ForEach(rects, rect => FillRectangle(brush, rect)); + + /// + /// Fills the interiors of a series of rectangles specified by structures. + /// + public void FillRectangles(Brush brush, params Rectangle[] rects) + => FillRectangles(brush, Array.ConvertAll(rects, rect => new RectangleF(rect.m_rect))); + + /// + /// Fills the interior of a . + /// + public void FillRegion(Brush brush, Region region) + => m_canvas.DrawRegion(region.m_region, brush.m_paint); + + /// + /// Forces execution of all pending graphics operations with the method waiting + /// or not waiting, as specified, to return before the operations finish. + /// + public void Flush(FlushIntention intention = FlushIntention.Flush) + { + if (intention == FlushIntention.Sync) + throw new NotSupportedException($"skia unsupported feature (param: {intention})"); + m_canvas.Flush(); + } + + /// + /// Gets a handle to the current Windows halftone palette. + /// + public static IntPtr GetHalftonePalette() + => throw new NotSupportedException("skia unsupported feature"); + + /// + /// Gets the handle to the device context associated with this . + /// + public IntPtr GetHdc() + => throw new NotSupportedException("skia unsupported feature"); + + /// + /// Gets the nearest color to the specified structure. + /// + public Color GetNearestColor(Color color) + { + var closest = SKColor.Empty; + var pivot = double.MaxValue; + + foreach (var candidate in m_bitmap.Pixels.Distinct()) + { + var powDiffs = new double[] + { + Math.Pow(color.R - candidate.Red, 2), + Math.Pow(color.G - candidate.Green, 2), + Math.Pow(color.B - candidate.Blue, 2), + Math.Pow(color.A - candidate.Alpha, 2) + }; + + var distance = Math.Sqrt(powDiffs.Sum()); + if (distance < pivot) + { + closest = candidate; + pivot = distance; + } + } + return new Color(closest); + } + + /// + /// Updates the clip region of this to the intersection + /// of the current clip region and the specified . + /// + public void IntersectClip(Region region) + => SetClip(region, CombineMode.Intersect); // TODO: implement Region + + /// + /// Updates the clip region of this to the intersection + /// of the current clip region and the specified structure. + /// + public void IntersectClip(RectangleF rect) + => IntersectClip(new Region(rect)); + + /// + /// Updates the clip region of this to the intersection + /// of the current clip region and the specified structure. + /// + public void IntersectClip(Rectangle rect) + => IntersectClip(new RectangleF(rect.m_rect)); + + /// + /// Indicates whether the specified structure is contained + /// within the visible clip region of this . + /// + public bool IsVisible(PointF point) + => m_canvas.LocalClipBounds.Contains(point.m_point); + + /// + /// Indicates whether the specified structure is contained + /// within the visible clip region of this . + /// + public bool IsVisible(Point point) + => IsVisible(point.X, point.Y); + + /// + /// Indicates whether the specified structure is contained + /// within the visible clip region of this . + /// + public bool IsVisible(RectangleF rect) + => m_canvas.LocalClipBounds.Contains(rect.m_rect); + + /// + /// Indicates whether the specified structure is contained + /// within the visible clip region of this . + /// + public bool IsVisible(Rectangle rect) + => IsVisible(rect.X, rect.Y, rect.Width, rect.Height); + + /// + /// Indicates whether the point specified by a pair of coordinates structure is contained + /// within the visible clip region of this . + /// + public bool IsVisible(int x, int y) + => IsVisible(new Point(x, y)); + + /// + /// Indicates whether the point specified by a pair of coordinates structure is contained + /// within the visible clip region of this . + /// + public bool IsVisible(float x, float y) + => IsVisible(new PointF(x, y)); + + /// + /// Indicates whether the specified by a pair of coordinates, a width, and a height is contained + /// within the visible clip region of this . + /// + public bool IsVisible(float x, float y, float width, float height) + => IsVisible(new RectangleF(x, y, width, height)); + + /// + /// Indicates whether the specified by a pair of coordinates, a width, and a height is contained + /// within the visible clip region of this . + /// + public bool IsVisible(int x, int y, int width, int height) + => IsVisible(new Rectangle(x, y, width, height)); + + /// + /// Gets an array of objects, each of which bounds a range of character + /// positions within the specified string. + /// + public Region[] MeasureCharacterRanges(ReadOnlySpan text, Font font, RectangleF layout, StringFormat format) + => MeasureCharacterRanges(new string(text.ToArray()), font, layout, format); + + /// + /// Gets an array of objects, each of which bounds a range of character + /// positions within the specified string. + /// + public Region[] MeasureCharacterRanges(string text, Font font, RectangleF layout, StringFormat format) + { + // TODO: implement StringFormat + if (text == null) throw new ArgumentNullException(nameof(text)); + if (font == null) throw new ArgumentNullException(nameof(font)); + + var localBounds = new RectangleF(m_canvas.LocalClipBounds); + var totalBounds = RectangleF.Intersect(localBounds, layout); + + var regions = new List(); + var lines = text.Split(StringFormat.BREAKLINES, StringSplitOptions.None); + + foreach (var line in lines) + { + foreach (var character in line) + { + var path = new GraphicsPath(); + path.AddString(character.ToString(), font.FontFamily, (int)font.Style, font.Size, totalBounds, format); + + var charBounds = path.GetBounds(); + var charRegion = new Region(charBounds); + regions.Add(charRegion); + } + } + + return regions.ToArray(); + } + + /// + /// Measures the specified string when drawn with the specified in a area, + /// formatted with the specified , and the maximum bounding box + /// specified by a structure. + /// + public SizeF MeasureString(string text, Font font, RectangleF layoutArea = default, StringFormat format = null) + => MeasureString(text, font, layoutArea.Size, format, out int _, out int _); + + /// + /// Measures the specified string when drawn with the specified in a area, formatted + /// with the specified , and a maximum layout area specified by + /// a structure. + /// + public SizeF MeasureString(string text, Font font, SizeF layoutArea, StringFormat format = null) + => MeasureString(text, font, new RectangleF(default, layoutArea), format); + + /// + /// Measures the specified string when drawn with the specified from an origin , formatted + /// with the specified , and the upper-left corner speficied by + /// a structure. + /// + public SizeF MeasureString(string text, Font font, PointF origin, StringFormat format = null) + => MeasureString(text, font, new RectangleF(origin, default), format); + + /// + /// Measures the specified string when drawn with the specified and certain width area, formatted + /// with the specified , and indicating the maximum width of the string. + /// + public SizeF MeasureString(string text, Font font, int width, StringFormat format = null) + => MeasureString(text, font, new SizeF(width, int.MaxValue), format); + + /// + /// Measures the specified string when drawn with the specified in a area, formatted + /// with the specified , and returning the structure, + /// the numbers of characters in the string, and the number of text lines in the string. + /// + public SizeF MeasureString(string text, Font font, SizeF layoutArea, StringFormat format, out int charsFitted, out int linesFilled) + => MeasureStringInternal(new ReadOnlySpan(text.ToArray()), font, new RectangleF(0, 0, layoutArea), format, out charsFitted, out linesFilled); + + /// + /// Measures the specified string when drawn with the specified in a area, formatted + /// with the specified , and the maximum bounding box + /// specified by a structure. + /// + public SizeF MeasureString(ReadOnlySpan text, Font font, RectangleF layoutArea = default, StringFormat format = null) + => MeasureString(new string(text.ToArray()), font, layoutArea, format); + + /// + /// Measures the specified string when drawn with the specified in a area, formatted + /// with the specified , and a maximum layout area specified by + /// a structure. + /// + public SizeF MeasureString(ReadOnlySpan text, Font font, SizeF layoutArea, StringFormat format = null) + => MeasureString(new string (text.ToArray()), font, layoutArea, format); + + /// + /// Measures the specified string when drawn with the specified from an origin , formatted + /// with the specified , and the upper-left corner speficied by + /// a structure. + /// + public SizeF MeasureString(ReadOnlySpan text, Font font, PointF origin, StringFormat format = null) + => MeasureString(new string(text.ToArray()), font, origin, format); + + /// + /// Measures the specified string when drawn with the specified and certail width area, formatted + /// with the specified , and indicating the maximum width of the string. + /// + public SizeF MeasureString(ReadOnlySpan text, Font font, int width, StringFormat format = null) + => MeasureString(new string(text.ToArray()), font, width, format); + + /// + /// Measures the specified string when drawn with the specified in a area, formatted + /// with the specified , and returning the structure, + /// the numbers of characters in the string, and the number of text lines in the string. + /// + public SizeF MeasureString(ReadOnlySpan text, Font font, SizeF layoutArea, StringFormat format, out int charsFitted, out int linesFilled) + => MeasureString(new string(text.ToArray()), font, layoutArea, format, out charsFitted, out linesFilled); + + /// + /// Measures the size of a string when drawn with the specified in a area, + /// formatted with the specified , and returning the structure, + /// the numbers of characters in the string, and the number of text lines in the string. + /// + public SizeF MeasureStringInternal(ReadOnlySpan text, Font font, RectangleF layoutArea, StringFormat format, out int charsFitted, out int linesFilled) + => MeasureStringInternal(new string(text.ToArray()), font, layoutArea, format, out charsFitted, out linesFilled); + + /// + /// Measures the size of a string when drawn with the specified in a area, + /// formatted with the specified , and returning the structure, + /// the numbers of characters in the string, and the number of text lines in the string. + /// + public SizeF MeasureStringInternal(string text, Font font, RectangleF layoutArea, StringFormat format, out int charsFitted, out int linesFilled) + { + var path = new GraphicsPath(); + path.AddString(text, font.FontFamily, (int)font.Style, font.Size, layoutArea, format); + + var bounds = path.GetBounds(); + + var charWidth = bounds.Width / text.Split(StringFormat.BREAKLINES, StringSplitOptions.None).Max(line => line.Length); + var availableWidth = Math.Min(layoutArea.Width, m_canvas.LocalClipBounds.Width); + charsFitted = (int)(availableWidth / charWidth); + + var lineHeight = bounds.Height; + var availableHeight = Math.Min(layoutArea.Height, m_canvas.LocalClipBounds.Height); + linesFilled = (int)(availableHeight / lineHeight); + + var width = Math.Min(bounds.Width, availableWidth); + var height = Math.Min(bounds.Height, availableHeight); + + return new SizeF(width, height); + } + + /// + /// Multiplies the world transformation of this and + /// speciried the in the specified order. + /// + public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend) + => m_canvas.SetMatrix(matrix.m_matrix); + + /// + /// Releases a device context handle obtained by a previous call to + /// the method of this . + /// + public void ReleaseHdc() + => ReleaseHdc(IntPtr.Zero); + + /// + /// Releases a device context handle obtained by a previous call to + /// the method of this . + /// + public void ReleaseHdc(IntPtr hdc) + => throw new NotSupportedException(); + + /// + /// Releases a handle to a device context. + /// + public void ReleaseHdcInternal(IntPtr hdc) + => throw new NotSupportedException(); + + /// + /// Resets the clip region of this to an infinite region. + /// + public void ResetClip() + => SetClip(Rectangle.Empty); + + /// + /// Resets the world transformation matrix of this to + /// an the identity matrix. + /// + public void ResetTransform() + => m_canvas.ResetMatrix(); + + /// + /// Restores the state of this to the state + /// represented by a . + /// + public void Restore(object state) + => throw new NotImplementedException(); // TODO: implement GraphicsState class + + /// + /// Applies the specified rotation to the transformation matrix of this . + /// + public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend) + { + var scaleMatrix = SKMatrix.CreateRotationDegrees(angle); + if (order == MatrixOrder.Append) + scaleMatrix = scaleMatrix.PreConcat(m_canvas.TotalMatrix); + m_canvas.Concat(ref scaleMatrix); + } + + /// + /// Saves the current state of this and + /// identifies the saved state with a . + /// + public object Save() + => throw new NotImplementedException(); // TODO: implement GraphicsState + + /// + /// Applies the specified scaling operation to the transformation matrix of + /// this by prepending it to the object's transformation matrix. + /// + public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend) + { + var scaleMatrix = SKMatrix.CreateScale(sx, sy); + if (order == MatrixOrder.Append) + scaleMatrix = scaleMatrix.PreConcat(m_canvas.TotalMatrix); + m_canvas.Concat(ref scaleMatrix); + } + + /// + /// Sets the clipping region of this to the result + /// of the specified operation combining the current clip region and the + /// specified . + /// + public void SetClip(Region region, CombineMode combineMode = CombineMode.Replace) + { + switch (combineMode) + { + case CombineMode.Replace: + Clip = region; + break; + + case CombineMode.Union: + Clip.Union(region); + break; + + case CombineMode.Intersect: + Clip.Intersect(region); + break; + + case CombineMode.Exclude: + Clip.Exclude(region); + break; + + case CombineMode.Complement: + Clip.Complement(region); + break; + + case CombineMode.Xor: + Clip.Xor(region); + break; + }; + throw new ArgumentException($"{combineMode} value is not supported", nameof(combineMode)); + } + + /// + /// Sets the clipping region of this to the result + /// of the specified operation combining the current clip region and the + /// specified structure. + /// + public void SetClip(Rectangle rect, CombineMode combineMode = CombineMode.Replace) + => SetClip(new Region(rect), combineMode); + + /// + /// Sets the clipping region of this to the result + /// of the specified combining operation of the current region + /// and the Clip property of the specified . + /// + public void SetClip(Graphics g, CombineMode combineMode = CombineMode.Replace) + => SetClip(g.Clip, combineMode); + + /// + /// Sets the clipping region of this to the result + /// of the specified operation combining the current clip region and the + /// specified . + /// + public void SetClip(GraphicsPath path, CombineMode combineMode = CombineMode.Replace) + => SetClip(new Region(path), combineMode); + + /// + /// Transforms an array of points from one coordinate space to another using + /// the current world and page transformations of this . + /// + public void TransformPoints(CoordinateSpace destination, CoordinateSpace source, PointF[] points) + => TransformPoints(destination, source, points, p => p.m_point, p => new(p.X, p.Y)); + + /// + /// Transforms an array of points from one coordinate space to another using + /// the current world and page transformations of this . + /// + public void TransformPoints(CoordinateSpace destination, CoordinateSpace source, Point[] points) + => TransformPoints(destination, source, points, p => p.m_point, p => new((int)p.X, (int)p.Y)); + + /// + /// Translates the clipping region of this by specified + /// amounts in the horizontal and vertical directions. + /// + public void TranslateClip(float dx, float dy) + => Clip.Translate(dx, dy); + + /// + /// Changes the origin of the coordinate system by applying the specified translation to the + /// transformation matrix of this in the specified order. + /// + public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrder.Prepend) + { + var scaleMatrix = SKMatrix.CreateTranslation(dx, dy); + if (order == MatrixOrder.Append) + scaleMatrix = scaleMatrix.PreConcat(m_canvas.TotalMatrix); + m_canvas.Concat(ref scaleMatrix); + } + + #endregion + + + #region Utitlies + + private static float GetUnitFactor(float dpi, GraphicsUnit unit) => unit switch + { + GraphicsUnit.Pixel => 1f, + GraphicsUnit.Display => dpi / 72f, + GraphicsUnit.Point => dpi / 72f, + GraphicsUnit.Inch => dpi, + GraphicsUnit.Document => dpi / 300f, + GraphicsUnit.Millimeter => dpi / 25.4f, + _ => throw new ArgumentException($"{unit} unit not supported.", nameof(unit)), + }; + + #endregion + + + #region Helpers + + private static GraphicsPath GetClosedCurvePath(PointF[] points, FillMode fillMode, float tension) + { + // TODO: include tension + if (points.Length < 3) + throw new ArgumentException("invalid number of points for drawing a closed curve (at least 3)"); + + var path = new GraphicsPath(fillMode); + path.AddClosedCurve(points); + return path; + } + + private static GraphicsPath GetPiePath(RectangleF rect, float startAngle, float sweepAngle) + { + var path = new GraphicsPath(); + path.AddPie(rect, startAngle, sweepAngle); + return path; + } + + private static GraphicsPath GetPolygonPath(PointF[] points, FillMode fillMode) + { + var path = new GraphicsPath(fillMode); + path.AddPolygon(points); + return path; + } + + private void TransformPoints(CoordinateSpace destination, CoordinateSpace source, T[] points, Func getPoint, Func newPoint) + { + // TODO: implement CoordinateSpace + var matrix = m_canvas.TotalMatrix; + for (int i = 0; i < points.Length; i++) + { + var point = matrix.MapPoint(getPoint(points[i])); + points[i] = newPoint(point); + } + } + + #endregion +} diff --git a/src/Common/Rectangle.cs b/src/Common/Rectangle.cs index 6ae6bd9..9bbc717 100644 --- a/src/Common/Rectangle.cs +++ b/src/Common/Rectangle.cs @@ -46,8 +46,16 @@ public Rectangle(Point location, int width, int height) #region Operators + /// + /// Converts the specified to a . + /// public static explicit operator SKRect(Rectangle rect) => rect.m_rect; + /// + /// Creates a with the coordinates of the specified . + /// + public static implicit operator RectangleF(Rectangle rect) => new(rect.X, rect.Y, rect.Width, rect.Height); + /// /// Tests whether two objects have equal location and size. /// @@ -190,6 +198,17 @@ public Point Location /// public readonly bool IsEmpty => m_rect.IsEmpty; + /// + /// Gets a secuencie of that defines this . + /// + public readonly Point[] Points => new[] + { + new Point(Left, Top), + new Point(Right, Top), + new Point(Right, Bottom), + new Point(Left, Bottom) + }; + #endregion diff --git a/src/Common/Region.cs b/src/Common/Region.cs index 0334239..517010d 100644 --- a/src/Common/Region.cs +++ b/src/Common/Region.cs @@ -168,13 +168,13 @@ public void Exclude(GraphicsPath path) /// Gets a structure that represents a rectangle that bounds this /// on the drawing surface of a object. /// - public Rectangle GetBounds(object g) - => new(m_region.Bounds); // TODO: consider g + public Rectangle GetBounds(Graphics g) + => new(SKRect.Intersect(m_region.Bounds, g.m_canvas.LocalClipBounds)); /// /// Returns a Windows handle to this in the specified graphics context. /// - public IntPtr GetHrgn(object g) + public IntPtr GetHrgn(Graphics g) => throw new NotSupportedException("windows specific"); /// @@ -261,71 +261,71 @@ public void Intersect(GraphicsPath path) /// Tests whether this has an empty interior on the specified drawing surface /// of a object. /// - public bool IsEmpty(object g) // TODO: consider g - => m_region.IsEmpty; + public bool IsEmpty(Graphics g) + => m_region.IsEmpty; // TODO: consider g /// /// Tests whether this has an infinite interior on the specified drawing surface /// of a object. /// - public bool IsInfinite(object g) - => m_region.IsRect && m_region.Bounds.Width == int.MaxValue && m_region.Bounds.Width == int.MaxValue; + public bool IsInfinite(Graphics g) + => m_region.IsRect && m_region.Bounds.Width == int.MaxValue && m_region.Bounds.Width == int.MaxValue; // TODO: consider g /// /// Tests whether any portion of the specified rectangle is contained within this when drawn /// using the specified (if it is defined). /// - public bool IsVisible(int x, int y, int width, int height, object g = null) + public bool IsVisible(int x, int y, int width, int height, Graphics g = null) => IsVisible(new Rectangle(x, y, width, height), g); /// /// Tests whether any portion of the specified rectangle is contained within this when drawn /// using the specified (if it is defined). /// - public bool IsVisible(float x, float y, float width, float height, object g = null) + public bool IsVisible(float x, float y, float width, float height, Graphics g = null) => IsVisible(new RectangleF(x, y, width, height), g); /// /// Tests whether any portion of the specified structure is contained within /// this when drawn using the specified (if it is defined). /// - public bool IsVisible(RectangleF rect, object g = null) + public bool IsVisible(RectangleF rect, Graphics g = null) => IsVisible(RectangleF.Truncate(rect), g); /// /// Tests whether any portion of the specified structure is contained within /// this when drawn using the specified (if it is defined). /// - public bool IsVisible(Rectangle rect, object g = null) - => m_region.Contains(SKRectI.Round(rect.m_rect)); + public bool IsVisible(Rectangle rect, Graphics g = null) + => m_region.Contains(SKRectI.Round(rect.m_rect)); // TODO: consider g /// /// Tests whether the specified point is contained within this when drawn /// using the specified (if it is defined). /// - public bool IsVisible(int x, int y, object g = null) + public bool IsVisible(int x, int y, Graphics g = null) => IsVisible(new Point(x, y), g); /// /// Tests whether the specified point is contained within this when drawn /// using the specified (if it is defined). /// - public bool IsVisible(float x, float y, object g = null) + public bool IsVisible(float x, float y, Graphics g = null) => IsVisible(new PointF(x, y), g); /// /// Tests whether the specified structure is contained within /// this when drawn using the specified (if it is defined). /// - public bool IsVisible(PointF point, object g = null) + public bool IsVisible(PointF point, Graphics g = null) => IsVisible(PointF.Truncate(point), g); /// /// Tests whether the specified structure is contained within /// this when drawn using the specified (if it is defined). /// - public bool IsVisible(Point point, object g = null) - => m_region.Contains(SKPointI.Round(point.m_point)); + public bool IsVisible(Point point, Graphics g = null) + => m_region.Contains(SKPointI.Round(point.m_point)); // TODO: consider g /// /// Initializes this to an empty interior. diff --git a/src/Common/Text/TextRenderingHint.cs b/src/Common/Text/TextRenderingHint.cs new file mode 100644 index 0000000..b7dcdb1 --- /dev/null +++ b/src/Common/Text/TextRenderingHint.cs @@ -0,0 +1,37 @@ +namespace GeneXus.Drawing.Text; + +/// +/// Specifies the quality of text rendering. +/// +public enum TextRenderingHint +{ + /// + /// Glyph with system default rendering hint. + /// + SystemDefault = 0, + + /// + /// Glyph bitmap with hinting. + /// + SingleBitPerPixelGridFit = 1, + + /// + /// Glyph bitmap without hinting. + /// + SingleBitPerPixel = 2, + + /// + /// Anti-aliasing with hinting. + /// + AntiAliasGridFit = 3, + + /// + /// Glyph anti-alias bitmap without hinting. + /// + AntiAlias = 4, + + /// + /// Glyph CT bitmap with hinting. + /// + ClearTypeGridFit = 5 +} From 78d235789426b72a82f1cea6a3ca25f7876eea2a Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 22:06:38 -0300 Subject: [PATCH 015/217] fix indentation spaces by tabs --- src/Common/CharacterRange.cs | 22 +- src/Common/Drawing2D/LineJoin.cs | 28 +-- src/Common/Drawing2D/PathData.cs | 22 +- src/Common/KnownColor.cs | 364 +++++++++++++++---------------- 4 files changed, 218 insertions(+), 218 deletions(-) diff --git a/src/Common/CharacterRange.cs b/src/Common/CharacterRange.cs index 4339f2f..69dea7a 100644 --- a/src/Common/CharacterRange.cs +++ b/src/Common/CharacterRange.cs @@ -48,7 +48,7 @@ public override readonly bool Equals(object obj) /// Returns the hash code for this instance. /// public override readonly int GetHashCode() - => Combine(First, Length); + => Combine(First, Length); #endregion @@ -68,18 +68,18 @@ public override readonly int GetHashCode() #endregion - #region Utilities + #region Utilities - private const uint PRIME1 = 2654435761U, PRIME2 = 2246822519U; + private const uint PRIME1 = 2654435761U, PRIME2 = 2246822519U; - private static int Combine(params object[] objects) - { - uint hash = PRIME1; - foreach (var obj in objects) - hash = hash * PRIME2 + (uint)obj.GetHashCode(); - return Convert.ToInt32(hash); - } + private static int Combine(params object[] objects) + { + uint hash = PRIME1; + foreach (var obj in objects) + hash = hash * PRIME2 + (uint)obj.GetHashCode(); + return Convert.ToInt32(hash); + } - #endregion + #endregion } diff --git a/src/Common/Drawing2D/LineJoin.cs b/src/Common/Drawing2D/LineJoin.cs index 24bf17a..5f5c51e 100644 --- a/src/Common/Drawing2D/LineJoin.cs +++ b/src/Common/Drawing2D/LineJoin.cs @@ -5,25 +5,25 @@ namespace GeneXus.Drawing.Drawing2D; /// public enum LineJoin { - /// - /// Specifies a mitered join. This produces a sharp corner or a clipped corner, depending on whether the - /// length of the miter exceeds the miter limit. - /// + /// + /// Specifies a mitered join. This produces a sharp corner or a clipped corner, depending on whether the + /// length of the miter exceeds the miter limit. + /// Miter = 0, - /// - /// Specifies a beveled join. This produces a diagonal corner. - /// + /// + /// Specifies a beveled join. This produces a diagonal corner. + /// Bevel = 1, - /// - /// pecifies a circular join. This produces a smooth, circular arc between the lines. - /// + /// + /// pecifies a circular join. This produces a smooth, circular arc between the lines. + /// Round = 2, - /// - /// Specifies a mitered join. This produces a sharp corner or a beveled corner, depending on whether the - /// length of the miter exceeds the miter limit. - /// + /// + /// Specifies a mitered join. This produces a sharp corner or a beveled corner, depending on whether the + /// length of the miter exceeds the miter limit. + /// MiterClipped = 3 } diff --git a/src/Common/Drawing2D/PathData.cs b/src/Common/Drawing2D/PathData.cs index b81b12e..a0c0aea 100644 --- a/src/Common/Drawing2D/PathData.cs +++ b/src/Common/Drawing2D/PathData.cs @@ -3,23 +3,23 @@ namespace GeneXus.Drawing.Drawing2D; public sealed class PathData { - /// - /// Initializes a new instance of the class. - /// + /// + /// Initializes a new instance of the class. + /// public PathData() { } - #region Properties + #region Properties - /// - /// Gets or sets an array of structures that represents the points through which the path is constructed. - /// + /// + /// Gets or sets an array of structures that represents the points through which the path is constructed. + /// public PointF[] Points { get; set; } - /// - /// Gets or sets the types of the corresponding points in the path. - /// + /// + /// Gets or sets the types of the corresponding points in the path. + /// public byte[] Types { get; set; } - #endregion + #endregion } \ No newline at end of file diff --git a/src/Common/KnownColor.cs b/src/Common/KnownColor.cs index 50b9151..987c56b 100644 --- a/src/Common/KnownColor.cs +++ b/src/Common/KnownColor.cs @@ -7,189 +7,189 @@ namespace GeneXus.Drawing; **/ public enum KnownColor - { - // 0 - reserved for "not a known color" + { + // 0 - reserved for "not a known color" - // "System" colors, Part 1 - ActiveBorder = 1, - ActiveCaption, - ActiveCaptionText, - AppWorkspace, - Control, - ControlDark, - ControlDarkDark, - ControlLight, - ControlLightLight, - ControlText, - Desktop, - GrayText, - Highlight, - HighlightText, - HotTrack, - InactiveBorder, - InactiveCaption, - InactiveCaptionText, - Info, - InfoText, - Menu, - MenuText, - ScrollBar, - Window, - WindowFrame, - WindowText, + // "System" colors, Part 1 + ActiveBorder = 1, + ActiveCaption, + ActiveCaptionText, + AppWorkspace, + Control, + ControlDark, + ControlDarkDark, + ControlLight, + ControlLightLight, + ControlText, + Desktop, + GrayText, + Highlight, + HighlightText, + HotTrack, + InactiveBorder, + InactiveCaption, + InactiveCaptionText, + Info, + InfoText, + Menu, + MenuText, + ScrollBar, + Window, + WindowFrame, + WindowText, - // "Web" Colors, Part 1 - Transparent, - AliceBlue, - AntiqueWhite, - Aqua, - Aquamarine, - Azure, - Beige, - Bisque, - Black, - BlanchedAlmond, - Blue, - BlueViolet, - Brown, - BurlyWood, - CadetBlue, - Chartreuse, - Chocolate, - Coral, - CornflowerBlue, - Cornsilk, - Crimson, - Cyan, - DarkBlue, - DarkCyan, - DarkGoldenrod, - DarkGray, - DarkGreen, - DarkKhaki, - DarkMagenta, - DarkOliveGreen, - DarkOrange, - DarkOrchid, - DarkRed, - DarkSalmon, - DarkSeaGreen, - DarkSlateBlue, - DarkSlateGray, - DarkTurquoise, - DarkViolet, - DeepPink, - DeepSkyBlue, - DimGray, - DodgerBlue, - Firebrick, - FloralWhite, - ForestGreen, - Fuchsia, - Gainsboro, - GhostWhite, - Gold, - Goldenrod, - Gray, - Green, - GreenYellow, - Honeydew, - HotPink, - IndianRed, - Indigo, - Ivory, - Khaki, - Lavender, - LavenderBlush, - LawnGreen, - LemonChiffon, - LightBlue, - LightCoral, - LightCyan, - LightGoldenrodYellow, - LightGray, - LightGreen, - LightPink, - LightSalmon, - LightSeaGreen, - LightSkyBlue, - LightSlateGray, - LightSteelBlue, - LightYellow, - Lime, - LimeGreen, - Linen, - Magenta, - Maroon, - MediumAquamarine, - MediumBlue, - MediumOrchid, - MediumPurple, - MediumSeaGreen, - MediumSlateBlue, - MediumSpringGreen, - MediumTurquoise, - MediumVioletRed, - MidnightBlue, - MintCream, - MistyRose, - Moccasin, - NavajoWhite, - Navy, - OldLace, - Olive, - OliveDrab, - Orange, - OrangeRed, - Orchid, - PaleGoldenrod, - PaleGreen, - PaleTurquoise, - PaleVioletRed, - PapayaWhip, - PeachPuff, - Peru, - Pink, - Plum, - PowderBlue, - Purple, - Red, - RosyBrown, - RoyalBlue, - SaddleBrown, - Salmon, - SandyBrown, - SeaGreen, - SeaShell, - Sienna, - Silver, - SkyBlue, - SlateBlue, - SlateGray, - Snow, - SpringGreen, - SteelBlue, - Tan, - Teal, - Thistle, - Tomato, - Turquoise, - Violet, - Wheat, - White, - WhiteSmoke, - Yellow, - YellowGreen, + // "Web" Colors, Part 1 + Transparent, + AliceBlue, + AntiqueWhite, + Aqua, + Aquamarine, + Azure, + Beige, + Bisque, + Black, + BlanchedAlmond, + Blue, + BlueViolet, + Brown, + BurlyWood, + CadetBlue, + Chartreuse, + Chocolate, + Coral, + CornflowerBlue, + Cornsilk, + Crimson, + Cyan, + DarkBlue, + DarkCyan, + DarkGoldenrod, + DarkGray, + DarkGreen, + DarkKhaki, + DarkMagenta, + DarkOliveGreen, + DarkOrange, + DarkOrchid, + DarkRed, + DarkSalmon, + DarkSeaGreen, + DarkSlateBlue, + DarkSlateGray, + DarkTurquoise, + DarkViolet, + DeepPink, + DeepSkyBlue, + DimGray, + DodgerBlue, + Firebrick, + FloralWhite, + ForestGreen, + Fuchsia, + Gainsboro, + GhostWhite, + Gold, + Goldenrod, + Gray, + Green, + GreenYellow, + Honeydew, + HotPink, + IndianRed, + Indigo, + Ivory, + Khaki, + Lavender, + LavenderBlush, + LawnGreen, + LemonChiffon, + LightBlue, + LightCoral, + LightCyan, + LightGoldenrodYellow, + LightGray, + LightGreen, + LightPink, + LightSalmon, + LightSeaGreen, + LightSkyBlue, + LightSlateGray, + LightSteelBlue, + LightYellow, + Lime, + LimeGreen, + Linen, + Magenta, + Maroon, + MediumAquamarine, + MediumBlue, + MediumOrchid, + MediumPurple, + MediumSeaGreen, + MediumSlateBlue, + MediumSpringGreen, + MediumTurquoise, + MediumVioletRed, + MidnightBlue, + MintCream, + MistyRose, + Moccasin, + NavajoWhite, + Navy, + OldLace, + Olive, + OliveDrab, + Orange, + OrangeRed, + Orchid, + PaleGoldenrod, + PaleGreen, + PaleTurquoise, + PaleVioletRed, + PapayaWhip, + PeachPuff, + Peru, + Pink, + Plum, + PowderBlue, + Purple, + Red, + RosyBrown, + RoyalBlue, + SaddleBrown, + Salmon, + SandyBrown, + SeaGreen, + SeaShell, + Sienna, + Silver, + SkyBlue, + SlateBlue, + SlateGray, + Snow, + SpringGreen, + SteelBlue, + Tan, + Teal, + Thistle, + Tomato, + Turquoise, + Violet, + Wheat, + White, + WhiteSmoke, + Yellow, + YellowGreen, - // "System" colors, Part 2 - ButtonFace, - ButtonHighlight, - ButtonShadow, - GradientActiveCaption, - GradientInactiveCaption, - MenuBar, - MenuHighlight, + // "System" colors, Part 2 + ButtonFace, + ButtonHighlight, + ButtonShadow, + GradientActiveCaption, + GradientInactiveCaption, + MenuBar, + MenuHighlight, - // "Web" colors, Part 2 - RebeccaPurple, - } \ No newline at end of file + // "Web" colors, Part 2 + RebeccaPurple, + } \ No newline at end of file From f2a2852aea234fb8d8198da09aa8109d1af43744 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 17 Jul 2024 23:54:10 -0300 Subject: [PATCH 016/217] fix Region.GetBounds signature and fix error when Graphics parameter is null --- src/Common/Region.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Region.cs b/src/Common/Region.cs index 517010d..ee1478a 100644 --- a/src/Common/Region.cs +++ b/src/Common/Region.cs @@ -168,8 +168,8 @@ public void Exclude(GraphicsPath path) /// Gets a structure that represents a rectangle that bounds this /// on the drawing surface of a object. /// - public Rectangle GetBounds(Graphics g) - => new(SKRect.Intersect(m_region.Bounds, g.m_canvas.LocalClipBounds)); + public RectangleF GetBounds(Graphics g) + => g == null ? new(m_region.Bounds) : new(SKRect.Intersect(m_region.Bounds, g.m_canvas.LocalClipBounds)); /// /// Returns a Windows handle to this in the specified graphics context. From ccf628321c276573edda91dd74eac38c5196825d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 18 Jul 2024 01:24:43 -0300 Subject: [PATCH 017/217] fix methods depending on Graphics class (TODOs) --- src/Common/Bitmap.cs | 8 ++++++-- src/Common/Region.cs | 42 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 0993b3e..1f8f996 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -53,8 +53,12 @@ public Bitmap(float width, float height) /// Initializes a new instance of the class with the specified size /// and with the resolution of the specified object. /// - public Bitmap(int width, int height, Graphics g) // TODO: Implement Graphics - : this(width, height) => throw new NotImplementedException(); + public Bitmap(int width, int height, Graphics g) + : this(width, height) + { + HorizontalResolution = g.DpiX; + VerticalResolution = g.DpiY; + } /// /// Initializes a new instance of the class with the specified size and format. diff --git a/src/Common/Region.cs b/src/Common/Region.cs index ee1478a..f43b1ac 100644 --- a/src/Common/Region.cs +++ b/src/Common/Region.cs @@ -169,7 +169,12 @@ public void Exclude(GraphicsPath path) /// on the drawing surface of a object. /// public RectangleF GetBounds(Graphics g) - => g == null ? new(m_region.Bounds) : new(SKRect.Intersect(m_region.Bounds, g.m_canvas.LocalClipBounds)); + { + var bounds = SKRect.Create(m_region.Bounds.Location, m_region.Bounds.Size); + if (g != null) + bounds.IntersectsWith(g.m_canvas.LocalClipBounds); + return new(bounds); + } /// /// Returns a Windows handle to this in the specified graphics context. @@ -220,16 +225,16 @@ public RegionData GetRegionData() } /// - /// Returns an array of structures that approximate this after + /// Returns an array of structures that approximate this after /// the specified matrix transformation is applied. /// - public Rectangle[] GetRegionScans(Matrix matrix) // TODO: apply Matrix + public RectangleF[] GetRegionScans(Matrix matrix) // TODO: apply Matrix { Transform(matrix); using var iterator = m_region.CreateRectIterator(); - var rects = new List(); + var rects = new List(); while (iterator.Next(out var rect)) - rects.Add(new Rectangle(rect)); + rects.Add(new RectangleF(rect)); return rects.ToArray(); } @@ -262,14 +267,24 @@ public void Intersect(GraphicsPath path) /// of a object. /// public bool IsEmpty(Graphics g) - => m_region.IsEmpty; // TODO: consider g + { + var region = new SKRegion(m_region); + if (g != null) + region.Intersects(g.Clip.m_region); + return region.IsEmpty; + } /// /// Tests whether this has an infinite interior on the specified drawing surface /// of a object. /// public bool IsInfinite(Graphics g) - => m_region.IsRect && m_region.Bounds.Width == int.MaxValue && m_region.Bounds.Width == int.MaxValue; // TODO: consider g + { + var region = new SKRegion(m_region); + if (g != null) + region.Intersects(g.Clip.m_region); + return !region.IsRect; + } /// /// Tests whether any portion of the specified rectangle is contained within this when drawn @@ -325,7 +340,18 @@ public bool IsVisible(PointF point, Graphics g = null) /// this when drawn using the specified (if it is defined). /// public bool IsVisible(Point point, Graphics g = null) - => m_region.Contains(SKPointI.Round(point.m_point)); // TODO: consider g + { + var region = new SKRegion(m_region); + if (g != null) + { + var points = new[] { point }; + g.Transform.TransformPoints(points); + point = points[0]; + + region.Intersects(g.Clip.m_region); + } + return region.Contains(point.X, point.Y); + } /// /// Initializes this to an empty interior. From b5d07bf925c0e812156e92bf26e695284e3ff2f4 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 18 Jul 2024 01:25:47 -0300 Subject: [PATCH 018/217] add Region class test-cases --- test/Common/RegionUnitTest.cs | 280 ++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 test/Common/RegionUnitTest.cs diff --git a/test/Common/RegionUnitTest.cs b/test/Common/RegionUnitTest.cs new file mode 100644 index 0000000..3bb95ea --- /dev/null +++ b/test/Common/RegionUnitTest.cs @@ -0,0 +1,280 @@ +using System; +using System.IO; +using GeneXus.Drawing.Drawing2D; + +namespace GeneXus.Drawing.Test; + +internal class RecgionUnitTest +{ + private static readonly string IMAGE_PATH = Path.Combine( + Directory.GetParent(Environment.CurrentDirectory).Parent.FullName, + "res", "images"); + + [Test] + public void Constructor_Default() + { + using var region = new Region(); + Assert.Multiple(() => + { + Assert.That(region, Is.Not.Null); + Assert.That(region.GetBounds(null), Is.EqualTo(RectangleF.Empty)); + }); + } + + [Test] + public void Constructor_RectangleF() + { + var rect = new RectangleF(0, 0, 100, 100); + + using var region = new Region(rect); + Assert.Multiple(() => + { + Assert.That(region, Is.Not.Null); + Assert.That(region.GetBounds(null), Is.EqualTo(rect)); + }); + } + + [Test] + public void Constructor_Rectangle() + { + var rectF = new RectangleF(0, 0, 100, 100); + var rectI = RectangleF.Truncate(rectF); + + using var region = new Region(rectI); + Assert.Multiple(() => + { + Assert.That(region, Is.Not.Null); + Assert.That(region.GetBounds(null), Is.EqualTo(rectF)); + }); + } + + [Test] + public void Constructor_GraphicsPath() + { + var rect = new RectangleF(0, 0, 100, 100); + + var path = new GraphicsPath(); + path.AddRectangle(rect); + + using var region = new Region(path); + Assert.Multiple(() => + { + Assert.That(region, Is.Not.Null); + Assert.That(region.GetBounds(null), Is.EqualTo(rect)); + }); + } + + [Test] + public void Constructor_RegionData() + { + var rect = new RectangleF(0, 0, 100, 100); + + using var initialRegion = new Region(rect); + var data = initialRegion.GetRegionData(); + + using var region = new Region(data); + Assert.Multiple(() => + { + Assert.That(region, Is.Not.Null); + Assert.That(region.GetBounds(null), Is.EqualTo(rect)); + }); + } + + [Test] + public void Method_Clone() + { + var rect = new RectangleF(0, 0, 100, 100); + + using var region = new Region(rect); + using var clone = region.Clone(); + + Assert.Multiple(() => + { + Assert.That(clone, Is.Not.Null); + Assert.That(clone, Is.TypeOf()); + Assert.That(clone, Is.Not.SameAs(region)); + Assert.That(clone.GetBounds(null), Is.EqualTo(rect)); + }); + } + + [Test] + public void Method_Complement_RectangleF() + { + using var region1 = new Region(new RectangleF(0, 0, 100, 100)); + using var region2 = new Region(new RectangleF(50, 50, 100, 100)); + + region1.Complement(region2); + Assert.That(region1.IsVisible(50, 50), Is.False); + } + + [Test] + public void Method_Complement_GraphicsPath() + { + var path = new GraphicsPath(); + path.AddRectangle(new RectangleF(50, 50, 100, 100)); + + using var region = new Region(new RectangleF(0, 0, 100, 100)); + + region.Complement(path); + Assert.That(region.IsVisible(50, 50), Is.False); + } + + [Test] + public void Method_Intersect_Region() + { + using var region1 = new Region(new RectangleF(0, 0, 100, 100)); + using var region2 = new Region(new RectangleF(50, 50, 100, 100)); + + region1.Intersect(region2); + Assert.That(region1.IsVisible(75, 75), Is.True); + } + + [Test] + public void Method_Intersect_RectangleF() + { + using var region = new Region(new RectangleF(0, 0, 100, 100)); + + region.Intersect(new RectangleF(50, 50, 100, 100)); + Assert.That(region.IsVisible(75, 75), Is.True); + } + + [Test] + public void Method_Intersect_GraphicsPath() + { + var path = new GraphicsPath(); + path.AddRectangle(new RectangleF(50, 50, 100, 100)); + + using var region = new Region(new RectangleF(0, 0, 100, 100)); + + region.Intersect(path); + Assert.That(region.IsVisible(75, 75), Is.True); + } + + [Test] + public void Method_Union_Region() + { + using var region1 = new Region(new RectangleF(0, 0, 100, 100)); + using var region2 = new Region(new RectangleF(50, 50, 100, 100)); + + region1.Union(region2); + Assert.That(region1.IsVisible(75, 75), Is.True); + Assert.That(region1.IsVisible(25, 25), Is.True); + } + + [Test] + public void Method_Union_RectangleF() + { + using var region = new Region(new RectangleF(0, 0, 100, 100)); + + region.Union(new RectangleF(50, 50, 100, 100)); + Assert.That(region.IsVisible(75, 75), Is.True); + Assert.That(region.IsVisible(25, 25), Is.True); + } + + [Test] + public void Method_Union_GraphicsPath() + { + var path = new GraphicsPath(); + path.AddRectangle(new RectangleF(50, 50, 100, 100)); + + using var region = new Region(new RectangleF(0, 0, 100, 100)); + + region.Union(path); + Assert.That(region.IsVisible(75, 75), Is.True); + Assert.That(region.IsVisible(25, 25), Is.True); + } + + [Test] + public void Method_Xor_Region() + { + using var region1 = new Region(new RectangleF(0, 0, 100, 100)); + using var region2 = new Region(new RectangleF(50, 50, 100, 100)); + + region1.Xor(region2); + Assert.That(region1.IsVisible(75, 75), Is.False); + Assert.That(region1.IsVisible(25, 25), Is.True); + Assert.That(region1.IsVisible(125, 125), Is.True); + } + + [Test] + public void Method_Xor_RectangleF() + { + using var region = new Region(new RectangleF(0, 0, 100, 100)); + + region.Xor(new RectangleF(50, 50, 100, 100)); + Assert.That(region.IsVisible(75, 75), Is.False); + Assert.That(region.IsVisible(25, 25), Is.True); + Assert.That(region.IsVisible(125, 125), Is.True); + } + + [Test] + public void Method_Xor_GraphicsPath() + { + var path = new GraphicsPath(); + path.AddRectangle(new RectangleF(50, 50, 100, 100)); + + using var region = new Region(new RectangleF(0, 0, 100, 100)); + + region.Xor(path); + Assert.That(region.IsVisible(75, 75), Is.False); + Assert.That(region.IsVisible(25, 25), Is.True); + Assert.That(region.IsVisible(125, 125), Is.True); + } + + [Test] + public void Method_GetBounds_Graphics() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); + using var image = Image.FromFile(filePath); + using var graphics = Graphics.FromImage(image); + + var rect = new RectangleF(0, 0, 100, 100); + using var region = new Region(rect); + + var bounds = region.GetBounds(graphics); + Assert.That(bounds, Is.EqualTo(rect)); + } + + [Test] + public void Method_GetRegionData() + { + using var region = new Region(new RectangleF(0, 0, 100, 100)); + var data = region.GetRegionData(); + + Assert.That(data, Is.Not.Null); + } + + [Test] + public void Method_Transform_Matrix() + { + using var region = new Region(new RectangleF(0, 0, 100, 100)); + + var matrix = new Matrix(); + matrix.Translate(50, 50); + + region.Transform(matrix); + Assert.That(region.IsVisible(75, 75), Is.True); + } + + [Test] + public void Method_Translate() + { + using var region = new Region(new RectangleF(0, 0, 100, 100)); + + region.Translate(50, 50); + Assert.That(region.IsVisible(75, 75), Is.True); + } + + [Test] + public void Method_GetRegionScans() + { + var rect = new RectangleF(0, 0, 100, 100); + + using var region = new Region(rect); + var scans = region.GetRegionScans(new Matrix()); + + Assert.That(scans, Is.Not.Null); + Assert.That(scans.Length, Is.EqualTo(1)); + Assert.That(scans[0], Is.EqualTo(rect)); + } +} \ No newline at end of file From a48994a6976d1fca73334c3a4725f9369b8eaed6 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 18 Jul 2024 11:53:57 -0300 Subject: [PATCH 019/217] fix Region.IsVisible method considering Graphics parameter --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- src/Common/Region.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 88a0203..add65d8 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -602,7 +602,7 @@ public bool IsVisible(int x, int y, Graphics g = null) /// Indicates whether the specified structure is /// contained within this . /// - public bool IsVisible(Point point, Graphics g = null) // TODO: consider Graphics + public bool IsVisible(Point point, Graphics g = null) => IsVisible(point.m_point, g?.m_canvas.LocalClipBounds); /// diff --git a/src/Common/Region.cs b/src/Common/Region.cs index f43b1ac..5b4a7aa 100644 --- a/src/Common/Region.cs +++ b/src/Common/Region.cs @@ -312,7 +312,12 @@ public bool IsVisible(RectangleF rect, Graphics g = null) /// this when drawn using the specified (if it is defined). /// public bool IsVisible(Rectangle rect, Graphics g = null) - => m_region.Contains(SKRectI.Round(rect.m_rect)); // TODO: consider g + { + var region = new SKRegion(m_region); + if (g != null) + region.Intersects(g.Clip.m_region); + return region.Contains(SKRectI.Round(rect.m_rect)); + } /// /// Tests whether the specified point is contained within this when drawn From b8978e4ffa549a80233b6e6d183ca8c87e5c87fe Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 19 Jul 2024 09:50:01 -0300 Subject: [PATCH 020/217] minor: remove unrequired usings in tests --- test/Common/BitmapUnitTest.cs | 1 - test/Common/Drawing2D/MatrixUnitTest.cs | 1 - test/Common/FontFamilyUnitTest.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/test/Common/BitmapUnitTest.cs b/test/Common/BitmapUnitTest.cs index 614bc05..3ab5a20 100644 --- a/test/Common/BitmapUnitTest.cs +++ b/test/Common/BitmapUnitTest.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using GeneXus.Drawing; namespace GeneXus.Drawing.Test; diff --git a/test/Common/Drawing2D/MatrixUnitTest.cs b/test/Common/Drawing2D/MatrixUnitTest.cs index 0aefc4c..1969b71 100644 --- a/test/Common/Drawing2D/MatrixUnitTest.cs +++ b/test/Common/Drawing2D/MatrixUnitTest.cs @@ -1,5 +1,4 @@ using GeneXus.Drawing.Drawing2D; -using System; using System.Numerics; namespace GeneXus.Drawing.Test.Drawing2D; diff --git a/test/Common/FontFamilyUnitTest.cs b/test/Common/FontFamilyUnitTest.cs index 30f59b9..69ab180 100644 --- a/test/Common/FontFamilyUnitTest.cs +++ b/test/Common/FontFamilyUnitTest.cs @@ -1,6 +1,5 @@ using GeneXus.Drawing.Text; using System; -using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; From f18809dfbff388ccb82db1df41f1159f027baca4 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 09:34:02 -0300 Subject: [PATCH 021/217] define PathPointType as byte --- src/Common/Drawing2D/PathPointType.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Common/Drawing2D/PathPointType.cs b/src/Common/Drawing2D/PathPointType.cs index 9045710..de55c68 100644 --- a/src/Common/Drawing2D/PathPointType.cs +++ b/src/Common/Drawing2D/PathPointType.cs @@ -3,42 +3,42 @@ namespace GeneXus.Drawing.Drawing2D; /// /// Specifies the type of point in a object. /// -public enum PathPointType +public enum PathPointType : byte { /// /// Indicates that the point is the start of a figure. /// - Start = 0, + Start = 0x00, /// /// Indicates that the point is an endpoint of a line. /// - Line = 1, + Line = 0x01, /// /// Indicates that the point is an endpoint or a control point of a cubic Bézier spline. /// - Bezier = 3, + Bezier = 0x03, /// /// Masks all bits except for the three low-order bits, which indicate the point type. /// - PathTypeMask = 7, + PathTypeMask = 0x07, /// /// Not used. /// - DashMode = 16, + DashMode = 0x10, /// /// Specifies that the point is a marker. /// - PathMarker = 32, + PathMarker = 0x20, /// /// Specifies that the point is the last point in a closed subpath (figure). /// - CloseSubpath = 128, + CloseSubpath = 0x80, /// /// A cubic Bézier curve. From fe6666fc321d6084bd17cfb37bd5c1ba0f424b48 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 09:38:12 -0300 Subject: [PATCH 022/217] fix infinite recursion --- src/Common/Drawing2D/GraphicsPath.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index add65d8..6b17337 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -241,7 +241,7 @@ public void AddBeziers(params Point[] points) /// of the points in the array. /// public void AddClosedCurve(params PointF[] points) - => AddClosedCurve(points); + => AddClosedCurve(points, 0.5f); /// /// Adds a closed curve to this path defined by an array @@ -273,7 +273,7 @@ public void AddClosedCurve(Point[] points, float tension = 0.5f) /// travels through each of the points in the array. /// public void AddCurve(params PointF[] points) - => AddCurve(points); + => AddCurve(points, 0.5f); /// /// Adds a spline curve to the current figure defined @@ -297,7 +297,7 @@ public void AddCurve(PointF[] points, int offset, int numberOfSegments, float te /// travels through each of the points in the array. /// public void AddCurve(params Point[] points) - => AddCurve(points); + => AddCurve(points, 0.5f); /// /// Adds a spline curve to the current figure defined From e7b43ad3cc3bb6f6cc0638cff57af5d84cd46028 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 09:39:07 -0300 Subject: [PATCH 023/217] define helper methods as private --- src/Common/Drawing2D/GraphicsPath.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 6b17337..bb7974b 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -687,14 +687,14 @@ public void AddEllipse(SKRect rect) => m_path.AddOval(rect); - public void AddLine(SKPoint pt1, SKPoint pt2) + private void AddLine(SKPoint pt1, SKPoint pt2) { m_path.MoveTo(pt1); m_path.LineTo(pt2); } - public void AddPie(SKRect rect, float startAngle, float sweepAngle) + private void AddPie(SKRect rect, float startAngle, float sweepAngle) { m_path.AddArc(rect, startAngle, sweepAngle); m_path.LineTo(rect.MidX, rect.MidY); @@ -702,7 +702,7 @@ public void AddPie(SKRect rect, float startAngle, float sweepAngle) } - public void AddPolygon(SKPoint[] points) + private void AddPolygon(SKPoint[] points) { if (points.Length < 3) throw new ArgumentException("At least three points are required."); @@ -710,11 +710,11 @@ public void AddPolygon(SKPoint[] points) } - public void AddRectangle(SKRect rect) + private void AddRectangle(SKRect rect) => m_path.AddRect(rect); - public void AddString(string text, FontFamily family, int style, float emSize, SKRect layout, StringFormat format) + private void AddString(string text, FontFamily family, int style, float emSize, SKRect layout, StringFormat format) { format ??= new StringFormat(); @@ -776,8 +776,10 @@ public void AddString(string text, FontFamily family, int style, float emSize, S int relIndex = index - lineIndexOffset; if (isRightToLeft && relIndex == 0 && line[relIndex] == '…') relIndex += 1; // TODO: look for a better fix for this (in rtl) + float origin = paint.MeasureText(line.Substring(0, relIndex)); float length = paint.MeasureText(line.Substring(relIndex, 1)); + var underline = new SKRect( origin + rtlOffset, lineHeightOffset + underlineOffset, @@ -830,14 +832,14 @@ public void AddString(string text, FontFamily family, int style, float emSize, S } - public bool IsOutlineVisible(SKPoint point, SKPaint pen, SKRect? bounds) + private bool IsOutlineVisible(SKPoint point, SKPaint pen, SKRect? bounds) { bool isBoundContained = bounds?.Contains(point) ?? true; return isBoundContained && pen.GetFillPath(m_path).Contains(point.X, point.Y); } - public bool IsVisible(SKPoint point, SKRect? bounds) + private bool IsVisible(SKPoint point, SKRect? bounds) { bool isBoundContained = bounds?.Contains(point) ?? true; return isBoundContained && m_path.Contains(point.X, point.Y); From ad8f362e81f7a7d8206d7d0c6a4cbce80ca571cc Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 09:40:08 -0300 Subject: [PATCH 024/217] fix IsVisible and IsOutlineVisible helper methods --- src/Common/Drawing2D/GraphicsPath.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index bb7974b..a4c1f6e 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -835,14 +835,15 @@ private void AddString(string text, FontFamily family, int style, float emSize, private bool IsOutlineVisible(SKPoint point, SKPaint pen, SKRect? bounds) { bool isBoundContained = bounds?.Contains(point) ?? true; - return isBoundContained && pen.GetFillPath(m_path).Contains(point.X, point.Y); + var path = pen.GetFillPath(m_path) ?? m_path; + return isBoundContained && path.Contains(point.X, point.Y); } private bool IsVisible(SKPoint point, SKRect? bounds) { bool isBoundContained = bounds?.Contains(point) ?? true; - return isBoundContained && m_path.Contains(point.X, point.Y); + return isBoundContained && m_path.Bounds.Contains(point.X, point.Y); } #endregion From 2e3ed4d1d2fc0ecded5bbe79e219ac81a13cb37d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:03:27 -0300 Subject: [PATCH 025/217] fix GetBound method --- src/Common/Drawing2D/GraphicsPath.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index a4c1f6e..aa88b3a 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -524,7 +524,7 @@ public RectangleF GetBounds() /// when this path is transformed by the specified . /// public RectangleF GetBounds(Matrix matrix) - => GetBounds(matrix, new Pen(Brushes.Transparent)); + => GetBounds(matrix, new Pen(Color.Transparent, 0)); /// /// Returns a rectangle that bounds this @@ -533,10 +533,10 @@ public RectangleF GetBounds(Matrix matrix) /// public RectangleF GetBounds(Matrix matrix, Pen pen) { - var transformed = new GraphicsPath(m_path); + using var transformed = new GraphicsPath(m_path); transformed.Transform(matrix); - var bounds = pen.m_paint.GetFillPath(transformed.m_path).Bounds; - return new RectangleF(bounds); + using var fill = pen.m_paint.GetFillPath(transformed.m_path) ?? transformed.m_path; + return new RectangleF(fill.Bounds); } /// From 8ba0726b842e7f68b12daeb78bd3ed077af37c7a Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:10:56 -0300 Subject: [PATCH 026/217] fix PathType property and CreatePath helper method --- src/Common/Drawing2D/GraphicsPath.cs | 41 +++++++++++++++++----------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index aa88b3a..0b06bba 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -136,18 +136,28 @@ public byte[] PathTypes int index = 0; var points = new SKPoint[4]; + SKPathVerb verb; while ((verb = iterator.Next(points)) != SKPathVerb.Done) { - types[index] = (byte)(types[index] | verb switch + if (verb == SKPathVerb.Close) + { + types[index - 1] |= (byte)PathPointType.CloseSubpath; + continue; + } + + (int size, byte type) = verb switch { - SKPathVerb.Move => (byte)PathPointType.Start, - SKPathVerb.Line => (byte)PathPointType.Line, - SKPathVerb.Cubic => (byte)PathPointType.Bezier, - SKPathVerb.Close => (byte)PathPointType.CloseSubpath, + SKPathVerb.Move => (1, (byte)PathPointType.Start), + SKPathVerb.Line => (2, (byte)PathPointType.Line), + SKPathVerb.Conic => (3, (byte)PathPointType.Bezier), + SKPathVerb.Cubic => (3, (byte)PathPointType.Bezier), + SKPathVerb.Quad => (4, (byte)PathPointType.Bezier), _ => throw new NotImplementedException($"verb {verb}") - }); - index = Math.Min(++index, types.Length - 1); + }; + + for (int offset = 0; offset < size && index < types.Length; offset++) + types[index++] = type; } return types; @@ -868,7 +878,8 @@ private static SKPath CreatePath(PointF[] points, byte[] types, FillMode mode) for (int i = 0; i < points.Length; i++) { - switch ((PathPointType)types[i]) + var type = types[i] & (byte)PathPointType.PathTypeMask; + switch ((PathPointType)type) { case PathPointType.Start: path.MoveTo(points[i].m_point); @@ -885,16 +896,14 @@ private static SKPath CreatePath(PointF[] points, byte[] types, FillMode mode) i += 2; break; - case PathPointType.CloseSubpath: - path.Close(); - break; + default: + throw new ArgumentException($"unknown type 0x{type:X2} at index {i}", nameof(types)); + } - case PathPointType.PathTypeMask: - throw new NotImplementedException(); + if ((types[i] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath) + path.Close(); - case PathPointType.PathMarker: - throw new NotImplementedException(); - } + // TODO: consider PathPointType.PathMarker, PathPointType.DashMode } return path; From f77c4a9e394e9dcc6f247cbd7451dc7e681de5b8 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:13:32 -0300 Subject: [PATCH 027/217] minor fix --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 0b06bba..ff9607d 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -670,7 +670,7 @@ public void Widen(Pen pen, Matrix matrix = null, float flatness = 0.25f) #endregion - #region Skia + #region Helpers private void AddArc(SKRect rect, float startAngle, float sweepAngle) => m_path.AddArc(rect, startAngle, sweepAngle); From 8e9ac8ff5332155105ebdb56c1bbd2d98e1d7522 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:13:50 -0300 Subject: [PATCH 028/217] fix AddBezier helper method --- src/Common/Drawing2D/GraphicsPath.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index ff9607d..30e2fdd 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -677,7 +677,11 @@ private void AddArc(SKRect rect, float startAngle, float sweepAngle) private void AddBezier(SKPoint pt1, SKPoint pt2, SKPoint pt3, SKPoint pt4) - => m_path.CubicTo(pt1, pt2, pt3); + { + if (m_path.LastPoint != pt1) + m_path.MoveTo(pt1); + m_path.CubicTo(pt2, pt3, pt4); + } private void AddBeziers(params SKPoint[] points) From 6b1acb43074e165aaf74cab986cf1cc5bda6442e Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:14:03 -0300 Subject: [PATCH 029/217] fix AddLine helper method --- src/Common/Drawing2D/GraphicsPath.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 30e2fdd..f709e9f 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -703,7 +703,8 @@ public void AddEllipse(SKRect rect) private void AddLine(SKPoint pt1, SKPoint pt2) { - m_path.MoveTo(pt1); + if (m_path.LastPoint != pt1) + m_path.MoveTo(pt1); m_path.LineTo(pt2); } From 4d5bfef3134f04ae7f3158cba883e3bca51e5fb4 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:14:29 -0300 Subject: [PATCH 030/217] fix AddPie helper method --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index f709e9f..e467501 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -711,7 +711,7 @@ private void AddLine(SKPoint pt1, SKPoint pt2) private void AddPie(SKRect rect, float startAngle, float sweepAngle) { - m_path.AddArc(rect, startAngle, sweepAngle); + m_path.ArcTo(rect, startAngle, sweepAngle, false); m_path.LineTo(rect.MidX, rect.MidY); m_path.Close(); } From 4b9b28db55366f915e778ae5198008f64623b064 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:17:56 -0300 Subject: [PATCH 031/217] fix AddCurve and AddEllipse helper methods visibility --- src/Common/Drawing2D/GraphicsPath.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index e467501..f258217 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -693,11 +693,11 @@ private void AddBeziers(params SKPoint[] points) } - public void AddCurve(SKPoint[] points, float tension, bool closed) // TODO: implement tension + private void AddCurve(SKPoint[] points, float tension, bool closed) // TODO: implement tension => m_path.AddPoly(points, closed); - public void AddEllipse(SKRect rect) + private void AddEllipse(SKRect rect) => m_path.AddOval(rect); From 6187ee16475ce34889e1f1452122de29112cd01c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:19:00 -0300 Subject: [PATCH 032/217] fix AddCurve helper method --- src/Common/Drawing2D/GraphicsPath.cs | 66 +++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index f258217..adb2a67 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -694,7 +694,71 @@ private void AddBeziers(params SKPoint[] points) private void AddCurve(SKPoint[] points, float tension, bool closed) // TODO: implement tension - => m_path.AddPoly(points, closed); + { + if (points.Length < 2) + throw new ArgumentException("At least two points are required", nameof(points)); + + tension = Math.Max(0, tension); + + if (m_path.LastPoint != points[0]) + m_path.MoveTo(points[0]); + + if (points.Length == 2) + { + m_path.LineTo(points[1]); + return; + } + + // calculate and add cubic bézier curves + for (int i = 0; i < points.Length - 1; i++) + { + SKPoint p0 = points[i]; + SKPoint p3 = points[i + 1]; + + SKPoint p1, p2; + + if (i == 0) + { + // first segment + p1 = new(p0.X + (closed ? p0.X - p3.X : p3.X - p0.X) * tension / 3, p0.Y + (p3.Y - p0.Y) * tension / 3); + } + else + { + SKPoint pPrev = points[i - 1]; + p1 = new(p0.X + (p3.X - pPrev.X) * tension / 3, p0.Y + (p3.Y - pPrev.Y) * tension / 3); + } + + if (i == points.Length - 2) + { + // last segment + p2 = new(p3.X - (closed ? p0.X - p3.X : p3.X - p0.X) * tension / 3, p3.Y - (p3.Y - p0.Y) * tension / 3); + } + else + { + SKPoint pNext = points[i + 2]; + p2 = new(p3.X - (pNext.X - p0.X) * tension / 3, p3.Y - (pNext.Y - p0.Y) * tension / 3); + } + + // add cubic bézier curve + m_path.CubicTo(p1, p2, p3); + } + + if (closed) + { + // Close the path by adding a segment from the last point to the first point + SKPoint p0 = points[points.Length - 1]; + SKPoint p3 = points[0]; + + // Calculate control points for the closing segment + SKPoint pPrev = points[points.Length - 2]; + SKPoint p1 = new(p0.X - (p0.X - pPrev.X) * tension / 3, p0.Y + (p0.Y - pPrev.Y) * tension / 3); + SKPoint p2 = new(p3.X - (pPrev.X - p0.X) * tension / 3, p3.Y - (pPrev.Y - p0.Y) * tension / 3); + + // add the closing cubic bézier curve and close path + m_path.CubicTo(p1, p2, p3); + m_path.Close(); + } + } private void AddEllipse(SKRect rect) From 89f9cc6d6c19de3be060808484fefb9f48a3b15b Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:21:56 -0300 Subject: [PATCH 033/217] minor indentation fix --- src/Common/Drawing2D/GraphicsPath.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index adb2a67..abd7d0a 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -346,8 +346,8 @@ public void AddEllipse(int x, int y, int width, int height) => AddEllipse(new Rectangle(x, y, width, height)); /// - /// Adds an ellipse to the current path bounded by - /// a structure. + /// Adds an ellipse to the current path bounded by + /// a structure. /// public void AddEllipse(Rectangle rect) => AddEllipse(rect.m_rect); From 8f4b33555b7d186633b2a86be579f987df1c0e19 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 10:54:46 -0300 Subject: [PATCH 034/217] implement Flatten method --- src/Common/Drawing2D/GraphicsPath.cs | 161 ++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index abd7d0a..e47392e 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -521,7 +521,80 @@ public void Flatten() /// line segments. /// public void Flatten(Matrix matrix, float flatness = 0.25f) - => throw new NotImplementedException("skia unsupported feature"); + { + if (flatness <= 0) + throw new ArgumentException($"zero or negative value {flatness}", nameof(flatness)); + + if (m_path.PointCount == 0) + return; + + var path = new SKPath(m_path); + m_path.Reset(); + + using SKPath.RawIterator iterator = path.CreateRawIterator(); + + SKPathVerb pathVerb; + SKPoint firstPoint = new(0, 0), lastPoint = new(0, 0); + SKPoint[] points = new SKPoint[4]; + while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done) + { + switch (pathVerb) + { + case SKPathVerb.Move: + m_path.MoveTo(Transform(points[0])); + firstPoint = lastPoint = points[0]; + break; + + case SKPathVerb.Line: + var linePoints = FlattenLine(points[0], points[1]); + foreach (var pt in linePoints) + m_path.LineTo(Transform(pt)); + + lastPoint = points[1]; + break; + + case SKPathVerb.Quad: + var quadPoints = FlattenQuad(points[0], points[1], points[2]); + foreach (var pt in quadPoints) + m_path.LineTo(Transform(pt)); + + lastPoint = points[2]; + break; + + case SKPathVerb.Conic: + var conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight()); + foreach (var pt in conicPoints) + m_path.LineTo(Transform(pt)); + + lastPoint = points[2]; + break; + + case SKPathVerb.Cubic: + var cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]); + foreach (var pt in cubicPoints) + m_path.LineTo(Transform(pt)); + + lastPoint = points[3]; + break; + + case SKPathVerb.Close: + var closePoints = FlattenLine(lastPoint, firstPoint); + foreach (var pt in closePoints) + m_path.LineTo(Transform(pt)); + + firstPoint = lastPoint = new SKPoint(0, 0); + m_path.Close(); + break; + } + } + + SKPoint Transform(SKPoint point) + { + var points = new PointF[] { new(point) }; + matrix.TransformPoints(points); + return points[0].m_point; + } + } /// /// Returns a rectangle that bounds this . @@ -928,6 +1001,92 @@ private bool IsVisible(SKPoint point, SKRect? bounds) #endregion + #region Flatten + + /* + * NOTE: Code taken from: + * https://learn.microsoft.com/en-us/previous-versions/xamarin/xamarin-forms/user-interface/graphics/skiasharp/curves/information + */ + + private static SKPoint[] FlattenLine(SKPoint pt0, SKPoint pt1) + { + int count = (int)Math.Max(1, EuclideanDistance(pt0, pt1)); + var points = new SKPoint[count]; + + for (int i = 0; i < count; i++) + { + float t = (i + 1f) / count; + float x = (1 - t) * pt0.X + t * pt1.X; + float y = (1 - t) * pt0.Y + t * pt1.Y; + points[i] = new SKPoint(x, y); + } + + return points; + } + + private static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3) + { + int count = (int)Math.Max(1, EuclideanDistance(pt0, pt1) + EuclideanDistance(pt1, pt2) + EuclideanDistance(pt2, pt3)); + var points = new SKPoint[count]; + + for (int i = 0; i < count; i++) + { + float t = (i + 1f) / count; + float x = (1 - t) * (1 - t) * (1 - t) * pt0.X + + 3 * t * (1 - t) * (1 - t) * pt1.X + + 3 * t * t * (1 - t) * pt2.X + + t * t * t * pt3.X; + float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y + + 3 * t * (1 - t) * (1 - t) * pt1.Y + + 3 * t * t * (1 - t) * pt2.Y + + t * t * t * pt3.Y; + points[i] = new SKPoint(x, y); + } + + return points; + } + + private static SKPoint[] FlattenQuad(SKPoint pt0, SKPoint pt1, SKPoint pt2) + { + int count = (int)Math.Max(1, EuclideanDistance(pt0, pt1) + EuclideanDistance(pt1, pt2)); + var points = new SKPoint[count]; + + for (int i = 0; i < count; i++) + { + float t = (i + 1f) / count; + float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X; + float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y; + points[i] = new SKPoint(x, y); + } + + return points; + } + + private static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight) + { + int count = (int)Math.Max(1, EuclideanDistance(pt0, pt1) + EuclideanDistance(pt1, pt2)); + var points = new SKPoint[count]; + + for (int i = 0; i < count; i++) + { + float t = (i + 1f) / count; + float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t; + float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X; + float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y; + x /= denominator; + y /= denominator; + points[i] = new SKPoint(x, y); + } + + return points; + } + + private static double EuclideanDistance(SKPoint pt0, SKPoint pt1) + => Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2)); + + #endregion + + #region Utilities private static SKPath CreatePath(PointF[] points, byte[] types, FillMode mode) From 24d8237aa266e10fbac453086911b3c308c184c7 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 22 Jul 2024 15:38:39 -0300 Subject: [PATCH 035/217] simplify Flatten method --- src/Common/Drawing2D/GraphicsPath.cs | 192 +++++++-------------------- 1 file changed, 51 insertions(+), 141 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index e47392e..fb7b40c 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -527,72 +527,66 @@ public void Flatten(Matrix matrix, float flatness = 0.25f) if (m_path.PointCount == 0) return; + + var data = PathData; + matrix.TransformPoints(data.Points); - var path = new SKPath(m_path); - m_path.Reset(); + using var path = new GraphicsPath(data.Points, data.Types, FillMode); - using SKPath.RawIterator iterator = path.CreateRawIterator(); + Reset(); - SKPathVerb pathVerb; - SKPoint firstPoint = new(0, 0), lastPoint = new(0, 0); - SKPoint[] points = new SKPoint[4]; - while ((pathVerb = iterator.Next(points)) != SKPathVerb.Done) + for (int i = 0; i < path.PointCount; i++) { - switch (pathVerb) + var type = path.PathTypes[i] & (byte)PathPointType.PathTypeMask; + switch((PathPointType)type) { - case SKPathVerb.Move: - m_path.MoveTo(Transform(points[0])); - firstPoint = lastPoint = points[0]; - break; - - case SKPathVerb.Line: - var linePoints = FlattenLine(points[0], points[1]); - foreach (var pt in linePoints) - m_path.LineTo(Transform(pt)); - - lastPoint = points[1]; - break; - - case SKPathVerb.Quad: - var quadPoints = FlattenQuad(points[0], points[1], points[2]); - foreach (var pt in quadPoints) - m_path.LineTo(Transform(pt)); - - lastPoint = points[2]; + case PathPointType.Start: + m_path.MoveTo(path.PathPoints[i].m_point); break; - case SKPathVerb.Conic: - var conicPoints = FlattenConic(points[0], points[1], points[2], iterator.ConicWeight()); - foreach (var pt in conicPoints) - m_path.LineTo(Transform(pt)); - - lastPoint = points[2]; + case PathPointType.Line: + m_path.LineTo(path.PathPoints[i].m_point); break; - case SKPathVerb.Cubic: - var cubicPoints = FlattenCubic(points[0], points[1], points[2], points[3]); - foreach (var pt in cubicPoints) - m_path.LineTo(Transform(pt)); + case PathPointType.Bezier: + if (i + 2 >= path.PointCount + || (path.PathTypes[i + 1] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier + || (path.PathTypes[i + 2] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier) + throw new ArgumentException("invalid Bezier curve definition"); + + int count = (int)Math.Floor(1 / flatness); + + var pt0 = m_path.LastPoint; + var pt1 = path.PathPoints[i + 0].m_point; + var pt2 = path.PathPoints[i + 1].m_point; + var pt3 = path.PathPoints[i + 2].m_point; + + for (int offset = 0; offset < count; offset++) + { + double t = (offset + 1.0) / count; + double u = 1.0 - t; + + double u2 = Math.Pow(u, 2); + double u3 = Math.Pow(u, 3); + double t2 = Math.Pow(t, 2); + double t3 = Math.Pow(t, 3); + + double x = u3 * pt0.X + 3 * t * u2 * pt1.X + 3 * t2 * u * pt2.X + t3 * pt3.X; + double y = u3 * pt0.Y + 3 * t * u2 * pt1.Y + 3 * t2 * u * pt2.Y + t3 * pt3.Y; + + var point = new SKPoint((float)x, (float)y); + m_path.LineTo(point); + } - lastPoint = points[3]; + i += 2; break; - case SKPathVerb.Close: - var closePoints = FlattenLine(lastPoint, firstPoint); - foreach (var pt in closePoints) - m_path.LineTo(Transform(pt)); - - firstPoint = lastPoint = new SKPoint(0, 0); - m_path.Close(); - break; + default: + throw new ArgumentException($"unknown type 0x{type:X2} at index {i}", nameof(data.Types)); } - } - - SKPoint Transform(SKPoint point) - { - var points = new PointF[] { new(point) }; - matrix.TransformPoints(points); - return points[0].m_point; + + if ((path.PathTypes[i] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath) + m_path.Close(); } } @@ -1001,92 +995,6 @@ private bool IsVisible(SKPoint point, SKRect? bounds) #endregion - #region Flatten - - /* - * NOTE: Code taken from: - * https://learn.microsoft.com/en-us/previous-versions/xamarin/xamarin-forms/user-interface/graphics/skiasharp/curves/information - */ - - private static SKPoint[] FlattenLine(SKPoint pt0, SKPoint pt1) - { - int count = (int)Math.Max(1, EuclideanDistance(pt0, pt1)); - var points = new SKPoint[count]; - - for (int i = 0; i < count; i++) - { - float t = (i + 1f) / count; - float x = (1 - t) * pt0.X + t * pt1.X; - float y = (1 - t) * pt0.Y + t * pt1.Y; - points[i] = new SKPoint(x, y); - } - - return points; - } - - private static SKPoint[] FlattenCubic(SKPoint pt0, SKPoint pt1, SKPoint pt2, SKPoint pt3) - { - int count = (int)Math.Max(1, EuclideanDistance(pt0, pt1) + EuclideanDistance(pt1, pt2) + EuclideanDistance(pt2, pt3)); - var points = new SKPoint[count]; - - for (int i = 0; i < count; i++) - { - float t = (i + 1f) / count; - float x = (1 - t) * (1 - t) * (1 - t) * pt0.X - + 3 * t * (1 - t) * (1 - t) * pt1.X - + 3 * t * t * (1 - t) * pt2.X - + t * t * t * pt3.X; - float y = (1 - t) * (1 - t) * (1 - t) * pt0.Y - + 3 * t * (1 - t) * (1 - t) * pt1.Y - + 3 * t * t * (1 - t) * pt2.Y - + t * t * t * pt3.Y; - points[i] = new SKPoint(x, y); - } - - return points; - } - - private static SKPoint[] FlattenQuad(SKPoint pt0, SKPoint pt1, SKPoint pt2) - { - int count = (int)Math.Max(1, EuclideanDistance(pt0, pt1) + EuclideanDistance(pt1, pt2)); - var points = new SKPoint[count]; - - for (int i = 0; i < count; i++) - { - float t = (i + 1f) / count; - float x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X; - float y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y; - points[i] = new SKPoint(x, y); - } - - return points; - } - - private static SKPoint[] FlattenConic(SKPoint pt0, SKPoint pt1, SKPoint pt2, float weight) - { - int count = (int)Math.Max(1, EuclideanDistance(pt0, pt1) + EuclideanDistance(pt1, pt2)); - var points = new SKPoint[count]; - - for (int i = 0; i < count; i++) - { - float t = (i + 1f) / count; - float denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t; - float x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X; - float y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y; - x /= denominator; - y /= denominator; - points[i] = new SKPoint(x, y); - } - - return points; - } - - private static double EuclideanDistance(SKPoint pt0, SKPoint pt1) - => Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2)); - - #endregion - - #region Utilities private static SKPath CreatePath(PointF[] points, byte[] types, FillMode mode) @@ -1118,8 +1026,10 @@ private static SKPath CreatePath(PointF[] points, byte[] types, FillMode mode) break; case PathPointType.Bezier: - if (i + 2 >= points.Length || (PathPointType)types[i + 1] != PathPointType.Bezier || (PathPointType)types[i + 2] != PathPointType.Bezier) - throw new ArgumentException("Invalid Bezier curve definition."); + if (i + 2 >= points.Length + || (types[i + 1] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier + || (types[i + 2] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier) + throw new ArgumentException("invalid Bezier curve definition."); path.CubicTo(points[i].m_point, points[i + 1].m_point, points[i + 2].m_point); i += 2; break; From cd7f607ec15319efa9b6f272fe99aa82e5b2f82d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 23 Jul 2024 10:05:03 -0300 Subject: [PATCH 036/217] improve performance in Matrix.TransformPoints method --- src/Common/Drawing2D/Matrix.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Common/Drawing2D/Matrix.cs b/src/Common/Drawing2D/Matrix.cs index 19145c6..da24e14 100644 --- a/src/Common/Drawing2D/Matrix.cs +++ b/src/Common/Drawing2D/Matrix.cs @@ -262,7 +262,7 @@ public void Translate(float offsetX, float offsetY, MatrixOrder order = MatrixOr /// array of . /// public void TransformPoints(PointF[] points) - => TransformPoints(points, m_matrix, p => new(p), p => p.m_point); + => TransformPoints(points, Transpose(m_matrix), p => new(p), p => p.m_point); /// /// Applies the geometric transform represented by this to a specified @@ -348,10 +348,9 @@ private static SKMatrix Transpose(SKMatrix matrix) private static void TransformPoints(T[] points, SKMatrix matrix, Func newPoint, Func getPoint) { - var transpose = Transpose(matrix); for (int i = 0; i < points.Length; i++) { - var point = transpose.MapPoint(getPoint(points[i])); + var point = matrix.MapPoint(getPoint(points[i])); points[i] = newPoint(point); } } @@ -370,7 +369,7 @@ private void TransformVectors(T[] points, Func newPoint, Func Date: Wed, 24 Jul 2024 13:57:04 -0300 Subject: [PATCH 037/217] fix GraphicsPath.PathTypes property --- src/Common/Drawing2D/GraphicsPath.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index fb7b40c..1f732a0 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -137,27 +137,33 @@ public byte[] PathTypes int index = 0; var points = new SKPoint[4]; - SKPathVerb verb; - while ((verb = iterator.Next(points)) != SKPathVerb.Done) + int size = 0; byte type = 0x00; + while (true) { + var verb = iterator.Next(points); + if (verb == SKPathVerb.Close) { types[index - 1] |= (byte)PathPointType.CloseSubpath; + size = 0; continue; } - (int size, byte type) = verb switch + for (int i = 0; i < size && index < types.Length; i++) + types[index++] = type; + + if (verb == SKPathVerb.Done) + break; + + (size, type) = verb switch { SKPathVerb.Move => (1, (byte)PathPointType.Start), - SKPathVerb.Line => (2, (byte)PathPointType.Line), + SKPathVerb.Line => (1, (byte)PathPointType.Line), SKPathVerb.Conic => (3, (byte)PathPointType.Bezier), SKPathVerb.Cubic => (3, (byte)PathPointType.Bezier), SKPathVerb.Quad => (4, (byte)PathPointType.Bezier), _ => throw new NotImplementedException($"verb {verb}") }; - - for (int offset = 0; offset < size && index < types.Length; offset++) - types[index++] = type; } return types; From 44f2835f181bb3d3ceb7c0b2dd49bc54d5624aa4 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 14:47:03 -0300 Subject: [PATCH 038/217] add GraphicsPath.ToSvg extra method --- src/Common/Drawing2D/GraphicsPath.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 1f732a0..1e3ff80 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -743,6 +743,17 @@ public void Widen(Pen pen, Matrix matrix = null, float flatness = 0.25f) #endregion + #region Extras + + /// + /// Returns the SVG string associated with this . + /// + public string ToSvg() + => m_path.ToSvgPathData(); + + #endregion + + #region Helpers private void AddArc(SKRect rect, float startAngle, float sweepAngle) From 9e3e3946dffc340e94e811e9aa9ae720900db54e Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 18:57:08 -0300 Subject: [PATCH 039/217] fix GtaphicsPath.PathTypes property --- src/Common/Drawing2D/GraphicsPath.cs | 57 ++++++++++++++++------------ 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 1e3ff80..d8bc882 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -137,36 +137,45 @@ public byte[] PathTypes int index = 0; var points = new SKPoint[4]; - int size = 0; byte type = 0x00; - while (true) + SKPathVerb verb; + while ((verb = iterator.Next(points))!= SKPathVerb.Done) { - var verb = iterator.Next(points); - - if (verb == SKPathVerb.Close) + switch (verb) { - types[index - 1] |= (byte)PathPointType.CloseSubpath; - size = 0; - continue; + case SKPathVerb.Move: + AddType(1, 0, (byte)PathPointType.Start); + break; + + case SKPathVerb.Line: + AddType(1, 1, (byte)PathPointType.Line); + break; + + case SKPathVerb.Conic: + case SKPathVerb.Quad: + AddType(2, 1, (byte)PathPointType.Bezier); + break; + + case SKPathVerb.Cubic: + AddType(3, 1, (byte)PathPointType.Bezier); + break; + + case SKPathVerb.Close when index > 0: + types[index - 1] |= (byte)PathPointType.CloseSubpath; + break; } - - for (int i = 0; i < size && index < types.Length; i++) - types[index++] = type; - - if (verb == SKPathVerb.Done) - break; - - (size, type) = verb switch - { - SKPathVerb.Move => (1, (byte)PathPointType.Start), - SKPathVerb.Line => (1, (byte)PathPointType.Line), - SKPathVerb.Conic => (3, (byte)PathPointType.Bezier), - SKPathVerb.Cubic => (3, (byte)PathPointType.Bezier), - SKPathVerb.Quad => (4, (byte)PathPointType.Bezier), - _ => throw new NotImplementedException($"verb {verb}") - }; } return types; + + void AddType(int count, int offset, byte type) + { + if (index >= m_path.PointCount) + return; + if (points[offset] != m_path.Points[index]) + return; + for (int i = 0; i < count; i++) + types[index++] = type; + } } } From 53b957e4a6b8d4f30642c785453738dec612c574 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 19:01:47 -0300 Subject: [PATCH 040/217] fix GraphicsPath.Flatten method --- src/Common/Drawing2D/GraphicsPath.cs | 38 +++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index d8bc882..830fda9 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -568,36 +568,44 @@ public void Flatten(Matrix matrix, float flatness = 0.25f) || (path.PathTypes[i + 1] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier || (path.PathTypes[i + 2] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier) throw new ArgumentException("invalid Bezier curve definition"); - - int count = (int)Math.Floor(1 / flatness); - var pt0 = m_path.LastPoint; + var pt0 = path.PathPoints[i - 1].m_point; var pt1 = path.PathPoints[i + 0].m_point; var pt2 = path.PathPoints[i + 1].m_point; var pt3 = path.PathPoints[i + 2].m_point; + + float d1 = SKPoint.Distance(pt0, pt1); + float d2 = SKPoint.Distance(pt1, pt2); + float d3 = SKPoint.Distance(pt2, pt3); - for (int offset = 0; offset < count; offset++) + int count = (int)Math.Max(1, d1 + d2 + d3); + var lastPoint = pt0; + for (int offset = 1; offset < count; offset++) { - double t = (offset + 1.0) / count; - double u = 1.0 - t; + float t = (offset + 1f) / count; + float u = 1f - t; - double u2 = Math.Pow(u, 2); - double u3 = Math.Pow(u, 3); - double t2 = Math.Pow(t, 2); - double t3 = Math.Pow(t, 3); + float u2 = u * u; + float u3 = u * u2; + float t2 = t * t; + float t3 = t * t2; - double x = u3 * pt0.X + 3 * t * u2 * pt1.X + 3 * t2 * u * pt2.X + t3 * pt3.X; - double y = u3 * pt0.Y + 3 * t * u2 * pt1.Y + 3 * t2 * u * pt2.Y + t3 * pt3.Y; + float x = u3 * pt0.X + 3 * t * u2 * pt1.X + 3 * t2 * u * pt2.X + t3 * pt3.X; + float y = u3 * pt0.Y + 3 * t * u2 * pt1.Y + 3 * t2 * u * pt2.Y + t3 * pt3.Y; - var point = new SKPoint((float)x, (float)y); - m_path.LineTo(point); + var currPoint = new SKPoint(x, y); + if (SKPoint.Distance(lastPoint, currPoint) > 1 - flatness) + { + m_path.LineTo(currPoint); + lastPoint = currPoint; + } } i += 2; break; default: - throw new ArgumentException($"unknown type 0x{type:X2} at index {i}", nameof(data.Types)); + throw new NotImplementedException($"point type 0x{type:X2} is not supported at index {i}."); } if ((path.PathTypes[i] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath) From f51754008709dd538bc8bff36b0c4c5d4b088c50 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 19:02:19 -0300 Subject: [PATCH 041/217] add using in GraphicsPath.Reverse method --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 830fda9..88bb88c 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -717,7 +717,7 @@ public void Reset() /// public void Reverse() { - var path = new SKPath(m_path); + using var path = new SKPath(m_path); m_path.Reset(); m_path.AddPathReverse(path); } From 77c29bf5d3276c8b546418d983db1688de733071 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 19:02:43 -0300 Subject: [PATCH 042/217] remove comment --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 88bb88c..d11f578 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -794,7 +794,7 @@ private void AddBeziers(params SKPoint[] points) } - private void AddCurve(SKPoint[] points, float tension, bool closed) // TODO: implement tension + private void AddCurve(SKPoint[] points, float tension, bool closed) { if (points.Length < 2) throw new ArgumentException("At least two points are required", nameof(points)); From a60a427c6d9a50dd8fda0d1ce54c467eee804d92 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 19:03:29 -0300 Subject: [PATCH 043/217] minor change --- src/Common/Drawing2D/GraphicsPath.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index d11f578..b74def0 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -1064,7 +1064,9 @@ private static SKPath CreatePath(PointF[] points, byte[] types, FillMode mode) || (types[i + 1] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier || (types[i + 2] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier) throw new ArgumentException("invalid Bezier curve definition."); + path.CubicTo(points[i].m_point, points[i + 1].m_point, points[i + 2].m_point); + i += 2; break; From 10245cb4766c2e6f9356b718f4c48fe165bdf406 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 19:10:50 -0300 Subject: [PATCH 044/217] add unit test for GraphicsPath class --- test/Common/Drawing2D/GraphicsPathUnitTest.cs | 626 ++++++++++++++++++ 1 file changed, 626 insertions(+) create mode 100644 test/Common/Drawing2D/GraphicsPathUnitTest.cs diff --git a/test/Common/Drawing2D/GraphicsPathUnitTest.cs b/test/Common/Drawing2D/GraphicsPathUnitTest.cs new file mode 100644 index 0000000..f730cff --- /dev/null +++ b/test/Common/Drawing2D/GraphicsPathUnitTest.cs @@ -0,0 +1,626 @@ +using GeneXus.Drawing.Drawing2D; +using System; +using System.Linq; + +namespace GeneXus.Drawing.Test.Drawing2D; + +internal class GraphicsPathUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_Default() + { + using var path = new GraphicsPath(); + Assert.Multiple(() => + { + Assert.That(path.FillMode, Is.EqualTo(FillMode.Alternate)); + Assert.That(path.PointCount, Is.EqualTo(0)); + Assert.That(path.PathPoints, Is.Empty); + Assert.That(path.PathTypes, Is.Empty); + }); + } + + [Test] + public void Constructor_FillMode() + { + var fillMode = FillMode.Winding; + + using var path = new GraphicsPath(fillMode); + Assert.Multiple(() => + { + Assert.That(path.FillMode, Is.EqualTo(fillMode)); + Assert.That(path.PointCount, Is.EqualTo(0)); + Assert.That(path.PathPoints, Is.Empty); + Assert.That(path.PathTypes, Is.Empty); + }); + } + + [Test] + public void Constructor_PointsAndTypes() + { + var fillMode = FillMode.Winding; + PointF[] points = { new(0, 0), new(1, 1) }; + byte[] types = { (byte)PathPointType.Start, (byte)PathPointType.Line }; + + using var path = new GraphicsPath(points, types, fillMode); + Assert.Multiple(() => + { + Assert.That(path.FillMode, Is.EqualTo(fillMode)); + Assert.That(path.PointCount, Is.EqualTo(2)); + Assert.That(path.PathPoints, Is.EqualTo(points)); + Assert.That(path.PathTypes, Is.EqualTo(types)); + }); + } + + [Test] + public void Property_FillMode() + { + using var path = new GraphicsPath(); + path.FillMode = FillMode.Winding; + Assert.That(path.FillMode, Is.EqualTo(FillMode.Winding)); + } + + [Test] + public void Method_Clone() + { + var fillMode = FillMode.Winding; + + using var path = new GraphicsPath(fillMode); + path.AddLine(0, 0, 1, 1); + + var clone = path.Clone(); + Assert.Multiple(() => + { + Assert.That(clone, Is.TypeOf()); + Assert.That(clone, Is.Not.SameAs(path)); + + if (clone is GraphicsPath cloned) + { + Assert.That(cloned.FillMode, Is.EqualTo(path.FillMode)); + Assert.That(cloned.PointCount, Is.EqualTo(path.PointCount)); + Assert.That(cloned.PathPoints, Is.EqualTo(path.PathPoints)); + Assert.That(cloned.PathTypes, Is.EqualTo(path.PathTypes)); + + cloned.Dispose(); + } + }); + } + + [Test] + public void Method_AddArc() + { + var rect = new RectangleF(0, 0, 100, 100); + + using var path = new GraphicsPath(); + path.AddArc(rect, 45, 90); + Assert.Multiple(() => + { + // TODO: remove this assert and replace with below asserts when method has been fixed + Assert.That(path.PointCount, Is.GreaterThan(0)); + /*Assert.That(path.PointCount, Is.EqualTo(4)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(85.35534f, 85.35532f), + new PointF(65.82913f, 104.8815f), + new PointF(34.17089f, 104.8815f), + new PointF(14.64468f, 85.35535f) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier + }));*/ + }); + } + + [Test] + public void Method_AddBezier() + { + var points = new[] { new PointF(1, 5), new PointF(1, 1), new PointF(2, 1), new PointF(3, 0) }; + + using var path = new GraphicsPath(); + path.AddBezier(points[0], points[1], points[2], points[3]); + Assert.Multiple(() => + { + Assert.That(path.PointCount, Is.EqualTo(4)); + Assert.That(path.PathPoints, Is.EqualTo(points)); + Assert.That(path.PathTypes, Is.EqualTo(new [] + { + (byte)PathPointType.Start, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier + })); + }); + } + + [Test] + public void Method_AddClosedCurve() + { + var points = new[] { new PointF(0, 0), new PointF(1, 1), new PointF(2, 0) }; + + using var path = new GraphicsPath(); + path.AddClosedCurve(points); + Assert.Multiple(() => + { + Assert.That(path.PointCount, Is.EqualTo(10)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(0, 0), + new PointF(-0.16666667f, 0.16666667f), + new PointF(0.6666666f, 1), + new PointF(1, 1), + new PointF(1.3333334f, 1), + new PointF(2.1666667f, 0.16666667f), + new PointF(2, 0), + new PointF(1.8333334f, -0.16666667f), + new PointF(0.16666667f, -0.16666667f), + new PointF(0, 0) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier + (byte)PathPointType.CloseSubpath + })); + }); + } + + [Test] + public void Method_AddCurve() + { + var points = new[] { new PointF(0, 0), new PointF(1, 1), new PointF(2, 0) }; + + using var path = new GraphicsPath(); + path.AddCurve(points); + Assert.Multiple(() => + { + Assert.That(path.PointCount, Is.EqualTo(7)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(0, 0), + new PointF(0.16666667f, 0.16666667f), + new PointF(0.6666666f, 1), + new PointF(1, 1), + new PointF(1.3333334f, 1), + new PointF(1.8333334f, 0.16666667f), + new PointF(2, 0) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier + })); + }); + } + + [Test] + public void Method_AddEllipse() + { + var rect = new RectangleF(0, 0, 100, 50); + + using var path = new GraphicsPath(); + path.AddEllipse(rect); + Assert.Multiple(() => + { + // TODO: remove this assert and replace with below asserts when method has been fixed + Assert.That(path.PointCount, Is.GreaterThan(0)); + /*Assert.That(path.PointCount, Is.EqualTo(13)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(100, 25), + new PointF(100, 38.80712f), + new PointF(77.61423f, 50), + new PointF(50, 50), + new PointF(22.38576f, 50), + new PointF(0, 38.80712f), + new PointF(0, 25), + new PointF(0, 11.19288f), + new PointF(22.38576f, 0), + new PointF(50, 0), + new PointF(77.61423f, 0), + new PointF(100, 11.19288f), + new PointF(100, 25) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier | (byte)PathPointType.CloseSubpath + }));*/ + }); + } + + [Test] + public void Method_AddLine() + { + var pt1 = new PointF(0, 0); + var pt2 = new PointF(1, 1); + + using var path = new GraphicsPath(); + path.AddLine(pt1, pt2); + Assert.Multiple(() => + { + Assert.That(path.PointCount, Is.EqualTo(2)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(0, 0), + new PointF(1, 1) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Line + })); + }); + } + + [Test] + public void Method_AddPie() + { + var rect = new RectangleF(0, 0, 100, 50); + + using var path = new GraphicsPath(); + path.AddPie(rect, 0, 90); + Assert.Multiple(() => + { + // TODO: remove this assert and replace with below asserts when method has been fixed + Assert.That(path.PointCount, Is.GreaterThan(0)); + /*Assert.That(path.PointCount, Is.EqualTo(5)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(50, 25), + new PointF(99.99999f, 25), + new PointF(99.99999f, 38.80711f), + new PointF(77.61423f, 49.99999f), + new PointF(50, 50) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Line, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier + (byte)PathPointType.CloseSubpath + }));*/ + }); + } + + [Test] + public void Method_AddPolygon() + { + var points = new PointF[] { new(0, 0), new(1, 1), new(2, 0) }; + + using var path = new GraphicsPath(); + path.AddPolygon(points); + Assert.Multiple(() => + { + Assert.That(path.PointCount, Is.EqualTo(3)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(0, 0), + new PointF(1, 1), + new PointF(2, 0) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Line, + (byte)PathPointType.Line | (byte)PathPointType.CloseSubpath + })); + }); + } + + [Test] + public void Method_AddRectangle() + { + var rect = new RectangleF(0, 0, 100, 50); + + using var path = new GraphicsPath(); + path.AddRectangle(rect); + Assert.Multiple(() => + { + Assert.That(path.PointCount, Is.EqualTo(4)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(0, 0), + new PointF(100, 0), + new PointF(100, 50), + new PointF(0, 50) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line | (byte)PathPointType.CloseSubpath + })); + }); + } + + [Test] + public void Method_AddString() + { + var text = "x!"; + var font = new Font("Arial", 16); + var origin = new PointF(0, 0); + var format = new StringFormat(); + + using var path = new GraphicsPath(); + path.AddString(text, font.FontFamily, (int)font.Style, font.Size, origin, format); + Assert.Multiple(() => + { + // TODO: remove this assert and replace with below asserts when method has been fixed + Assert.That(path.PointCount, Is.GreaterThan(0)); + /*Assert.That(path.PointCount, Is.EqualTo(29)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(2.78125f, 14.48438f), + new PointF(5.8125f, 10.17188f), + new PointF(3.007813f, 6.1875f), + new PointF(4.765625f, 6.1875f), + new PointF(6.039063f, 8.132813f), + new PointF(6.278646f, 8.502604f), + new PointF(6.471354f, 8.8125f), + new PointF(6.617188f, 9.0625f), + new PointF(6.846354f, 8.71875f), + new PointF(7.057292f, 8.414063f), + new PointF(7.25f, 8.148438f), + new PointF(8.648438f, 6.1875f), + new PointF(10.32813f, 6.1875f), + new PointF(7.460938f, 10.09375f), + new PointF(10.54688f, 14.48438f), + new PointF(8.820313f, 14.48438f), + new PointF(7.117188f, 11.90625f), + new PointF(6.664063f, 11.21094f), + new PointF(4.484375f, 14.48438f), + new PointF(12.71094f, 11.64063f), + new PointF(12.28125f, 5.570313f), + new PointF(12.28125f, 3.03125f), + new PointF(14.02344f, 3.03125f), + new PointF(14.02344f, 5.570313f), + new PointF(13.61719f, 11.64063f), + new PointF(12.34375f, 14.48438f), + new PointF(12.34375f, 12.88281f), + new PointF(13.96094f, 12.88281f), + new PointF(13.96094f, 14.48438f) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line | (byte)PathPointType.DashMode | (byte)PathPointType.CloseSubpath, + (byte)PathPointType.Start, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line + (byte)PathPointType.CloseSubpath, + (byte)PathPointType.Start, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line + (byte)PathPointType.DashMode + (byte)PathPointType.CloseSubpath + }));*/ + }); + } + + [Test] + public void Method_CloseFigure() + { + var points = new[] { new PointF(0, 0), new PointF(1, 1), new PointF(2, 0) }; + + using var path = new GraphicsPath(); + path.AddCurve(points); + + path.CloseFigure(); + Assert.That((path.PathTypes[path.PointCount - 1] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath); + } + + [Test] + public void Method_Flatten() + { + var points = new[] { new PointF(1, 5), new PointF(1, 3), new PointF(5, 5), new PointF(5, 3) }; + + var matrix = new Matrix(); + matrix.Translate(5, 10); + + using var path = new GraphicsPath(); + path.AddBezier(points[0], points[1], points[2], points[3]); + + path.Flatten(matrix, 0.25f); + Assert.Multiple(() => + { + Assert.That(path.PointCount, Is.EqualTo(5)); + Assert.That(path.PathPoints, Is.EqualTo(new[] + { + new PointF(6f, 15f), + new PointF(6.625f, 14.125f), + new PointF(8f, 14f), + new PointF(9.375f, 13.875f), + new PointF(10f, 13f) + })); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line + })); + }); + } + + [Test] + public void Method_GetBounds() + { + var rect = new RectangleF(0, 0, 100, 50); + + using var path = new GraphicsPath(); + path.AddRectangle(rect); + + var bounds = path.GetBounds(); + Assert.Multiple(() => + { + Assert.That(bounds.X, Is.EqualTo(0)); + Assert.That(bounds.Y, Is.EqualTo(0)); + Assert.That(bounds.Width, Is.EqualTo(100)); + Assert.That(bounds.Height, Is.EqualTo(50)); + }); + } + + [Test] + public void Method_GetLastPoint() + { + var points = new[] { new PointF(0, 0), new PointF(1, 1), new PointF(2, 0) }; + + using var path = new GraphicsPath(); + path.AddCurve(points); + + Assert.That(path.GetLastPoint(), Is.EqualTo(points[points.Length - 1])); + } + + [Test] + public void Method_IsOutlineVisible() + { + var pen = new Pen(Color.Black, 1); + var rect = new RectangleF(0, 0, 3, 3); + + using var path = new GraphicsPath(); + path.AddRectangle(rect); + + Assert.Multiple(() => + { + Assert.That(path.IsOutlineVisible(0, 0, pen), Is.True); + Assert.That(path.IsOutlineVisible(2, 2, pen), Is.False); + Assert.That(path.IsOutlineVisible(3, 3, pen), Is.True); + Assert.That(path.IsOutlineVisible(4, 4, pen), Is.False); + }); + } + + [Test] + public void Method_IsVisible() + { + var rect = new RectangleF(0, 0, 3, 3); + + using var path = new GraphicsPath(); + path.AddRectangle(rect); + + Assert.Multiple(() => + { + Assert.That(path.IsVisible(0, 0), Is.True); + Assert.That(path.IsVisible(2, 2), Is.True); + Assert.That(path.IsVisible(3, 3), Is.False); + Assert.That(path.IsVisible(4, 4), Is.False); + }); + } + + [Test] + public void Method_Reverse() + { + var rect = new RectangleF(0, 0, 100, 50); + + using var path = new GraphicsPath(); + path.AddLine(0, 0, 1, 1); + path.AddLine(1, 1, 2, 2); + path.AddBezier(2, 2, 2, 3, 3, 3, 3, 4); + path.AddRectangle(rect); + + var reversedPoints = path.PathPoints.Reverse().ToArray(); + + path.Reverse(); + Assert.Multiple(() => + { + Assert.That(path.PathPoints, Is.EqualTo(reversedPoints)); + Assert.That(path.PathTypes, Is.EqualTo(new[] + { + (byte)PathPointType.Start, + (byte)PathPointType.Line, + (byte)PathPointType.Line, + (byte)PathPointType.Line | (byte)PathPointType.CloseSubpath, + (byte)PathPointType.Start, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Bezier, + (byte)PathPointType.Line, + (byte)PathPointType.Line + })); + }); + } + + [Test] + public void Method_Reset() + { + var rect = new RectangleF(0, 0, 100, 50); + + using var path = new GraphicsPath(); + path.AddRectangle(rect); + + path.Reset(); + Assert.Multiple(() => + { + Assert.That(path.PointCount, Is.EqualTo(0)); + Assert.That(path.PathPoints, Is.Empty); + Assert.That(path.PathTypes, Is.Empty); + }); + } + + [Test] + public void Method_Transform() + { + using var path = new GraphicsPath(); + path.AddLine(0, 0, 1, 1); + + using var matrix = new Matrix(); + matrix.Translate(1, 1); + + var transformedPoints = path.PathPoints; + matrix.TransformPoints(transformedPoints); + + path.Transform(matrix); + Assert.That(path.PathPoints, Is.EqualTo(transformedPoints)); + } +} \ No newline at end of file From 320644e6bee4152aedf730e46227af9ec27431ef Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 23:29:30 -0300 Subject: [PATCH 045/217] minor changes in test cases for Region and GraphicsPath --- test/Common/Drawing2D/GraphicsPathUnitTest.cs | 20 +++++++++---------- test/Common/RegionUnitTest.cs | 12 +++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/test/Common/Drawing2D/GraphicsPathUnitTest.cs b/test/Common/Drawing2D/GraphicsPathUnitTest.cs index f730cff..ba99721 100644 --- a/test/Common/Drawing2D/GraphicsPathUnitTest.cs +++ b/test/Common/Drawing2D/GraphicsPathUnitTest.cs @@ -69,21 +69,21 @@ public void Method_Clone() { var fillMode = FillMode.Winding; - using var path = new GraphicsPath(fillMode); - path.AddLine(0, 0, 1, 1); + using var path1 = new GraphicsPath(fillMode); + path1.AddLine(0, 0, 1, 1); - var clone = path.Clone(); + var path2 = path1.Clone(); Assert.Multiple(() => { - Assert.That(clone, Is.TypeOf()); - Assert.That(clone, Is.Not.SameAs(path)); + Assert.That(path2, Is.TypeOf()); + Assert.That(path2, Is.Not.SameAs(path1)); - if (clone is GraphicsPath cloned) + if (path2 is GraphicsPath cloned) { - Assert.That(cloned.FillMode, Is.EqualTo(path.FillMode)); - Assert.That(cloned.PointCount, Is.EqualTo(path.PointCount)); - Assert.That(cloned.PathPoints, Is.EqualTo(path.PathPoints)); - Assert.That(cloned.PathTypes, Is.EqualTo(path.PathTypes)); + Assert.That(cloned.FillMode, Is.EqualTo(path1.FillMode)); + Assert.That(cloned.PointCount, Is.EqualTo(path1.PointCount)); + Assert.That(cloned.PathPoints, Is.EqualTo(path1.PathPoints)); + Assert.That(cloned.PathTypes, Is.EqualTo(path1.PathTypes)); cloned.Dispose(); } diff --git a/test/Common/RegionUnitTest.cs b/test/Common/RegionUnitTest.cs index 3bb95ea..90227b9 100644 --- a/test/Common/RegionUnitTest.cs +++ b/test/Common/RegionUnitTest.cs @@ -85,15 +85,15 @@ public void Method_Clone() { var rect = new RectangleF(0, 0, 100, 100); - using var region = new Region(rect); - using var clone = region.Clone(); + using var region1 = new Region(rect); + using var region2 = region1.Clone(); Assert.Multiple(() => { - Assert.That(clone, Is.Not.Null); - Assert.That(clone, Is.TypeOf()); - Assert.That(clone, Is.Not.SameAs(region)); - Assert.That(clone.GetBounds(null), Is.EqualTo(rect)); + Assert.That(region2, Is.Not.Null); + Assert.That(region2, Is.TypeOf()); + Assert.That(region2, Is.Not.SameAs(region1)); + Assert.That(region2.GetBounds(null), Is.EqualTo(rect)); }); } From af2fdd00b82d59c394d3047272dbbfa6cb34f9fd Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 23:31:17 -0300 Subject: [PATCH 046/217] change definition for Pen properties: DashCap, DashOffset, DashPattern, StartCap, and EndCap --- src/Common/Pen.cs | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/src/Common/Pen.cs b/src/Common/Pen.cs index bda55d2..7df74cc 100644 --- a/src/Common/Pen.cs +++ b/src/Common/Pen.cs @@ -164,30 +164,17 @@ public float[] CompoundArray /// Gets or sets the cap style used at the end of the dashes that make /// up dashed lines drawn with this . /// - public DashCap DashCap - { - - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } + public DashCap DashCap { get; set; } = DashCap.Flat; /// /// Gets or sets the distance from the start of a line to the beginning of a dash pattern. /// - public float DashOffset - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } + public float DashOffset { get; set; } = 0.0f; /// /// Gets or sets an array of custom dashes and spaces. /// - public float[] DashPattern - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } + public float[] DashPattern { get; set; } = Array.Empty(); /// /// Gets the style of lines drawn with this . @@ -199,26 +186,18 @@ public float[] DashPattern LinearGradientBrush => PenType.LinearGradient, PathGradientBrush => PenType.PathGradient, HatchBrush => PenType.HatchFill, - _ => throw new NotImplementedException() + _ => throw new NotImplementedException($"the {m_brush.GetType().Name} pen type is not implemented.") }; /// /// Gets or sets the cap style used at the beginning of lines drawn with this . /// - public LineCap StartCap - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } + public LineCap StartCap { get; set; } = LineCap.Flat; /// /// Gets or sets the cap style used at the end of lines drawn with this . /// - public LineCap EndCap - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } + public LineCap EndCap { get; set; } = LineCap.Flat; /// /// Gets or sets the join style for the ends of two consecutive lines drawn with this . From b196009ba1459ec4f43f150edf3a9dc70fff01cb Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 24 Jul 2024 23:47:10 -0300 Subject: [PATCH 047/217] init proper values in Pen class --- src/Common/Pen.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Common/Pen.cs b/src/Common/Pen.cs index 7df74cc..218ac08 100644 --- a/src/Common/Pen.cs +++ b/src/Common/Pen.cs @@ -15,6 +15,8 @@ internal Pen(SKPaint paint, float width) m_paint = paint ?? throw new ArgumentNullException(nameof(paint)); m_paint.Style = SKPaintStyle.Stroke; m_paint.StrokeWidth = width; + m_paint.StrokeMiter = 10; + m_paint.TextAlign = SKTextAlign.Center; } /// @@ -136,10 +138,16 @@ public Color Color /// public float[] CompoundArray { - get => throw new NotImplementedException(); - set => m_paint.PathEffect = SKPathEffect.CreateDash(value, 0); + get => m_interval; + set + { + m_interval = value; + m_paint.PathEffect = SKPathEffect.CreateDash(m_interval, 0); + } } + private float[] m_interval = Array.Empty(); + /// /// Gets or sets a custom cap to use at the end of lines drawn /// with this . From 8eb90a1847aa5e988c26a60e707c4c558e9a39f3 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 10:15:24 -0300 Subject: [PATCH 048/217] minor improvement on exception message for Pen.Aligment property --- src/Common/Pen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Pen.cs b/src/Common/Pen.cs index 218ac08..7b60e89 100644 --- a/src/Common/Pen.cs +++ b/src/Common/Pen.cs @@ -99,14 +99,14 @@ public PenAlignment Alignment SKTextAlign.Center => PenAlignment.Center, SKTextAlign.Left => PenAlignment.Left, SKTextAlign.Right => PenAlignment.Right, - _ => throw new NotSupportedException("skia mapping") + _ => throw new NotSupportedException($"unsuported value {m_paint.TextAlign}") }; set => m_paint.TextAlign = value switch { PenAlignment.Center => SKTextAlign.Center, PenAlignment.Left => SKTextAlign.Left, PenAlignment.Right => SKTextAlign.Right, - _ => throw new NotSupportedException("skia mapping") + _ => throw new NotSupportedException($"unsuported value {value}") }; } From 5816f0c3ad93c0d42fc45b6227ab61573fd4f1b5 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 11:12:22 -0300 Subject: [PATCH 049/217] fix Pen.Brush property behaviour --- src/Common/Pen.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Common/Pen.cs b/src/Common/Pen.cs index 7b60e89..bd55f4b 100644 --- a/src/Common/Pen.cs +++ b/src/Common/Pen.cs @@ -8,7 +8,7 @@ namespace GeneXus.Drawing; public class Pen : ICloneable, IDisposable { internal SKPaint m_paint; - private Brush m_brush = Brushes.Transparent; + private Brush m_brush; internal Pen(SKPaint paint, float width) { @@ -116,11 +116,7 @@ public PenAlignment Alignment public Brush Brush { get => m_brush; - set - { - m_paint.Color = value.m_paint.Color; - m_brush = value; - } + set => m_brush = value; } /// @@ -129,7 +125,11 @@ public Brush Brush public Color Color { get => new(m_paint.Color); - set => m_paint.Color = value.m_color; + set + { + m_paint.Color = value.m_color; + m_brush.m_paint.Color = value.m_color; + } } /// From a0951eb4882c0bbe2ee04bf9d6e1b615e90e4591 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 12:13:34 -0300 Subject: [PATCH 050/217] fix Pen.Clone method --- src/Common/Pen.cs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Common/Pen.cs b/src/Common/Pen.cs index bd55f4b..95bcec5 100644 --- a/src/Common/Pen.cs +++ b/src/Common/Pen.cs @@ -72,7 +72,28 @@ public void Dispose() /// Creates an exact copy of this . /// public object Clone() - => new Pen(m_paint, m_paint.StrokeWidth); + => new Pen(Color, Width) + { + Alignment = Alignment, + Brush = Brush switch + { + SolidBrush sb => sb.Clone() as SolidBrush, + HatchBrush hb => hb.Clone() as HatchBrush, + TextureBrush tb => tb.Clone() as TextureBrush, + PathGradientBrush pgb => pgb.Clone() as PathGradientBrush, + LinearGradientBrush lgb => lgb.Clone() as LinearGradientBrush, + _ => throw new NotImplementedException($"undefined map to {Brush.GetType().Name}.") + }, + CompoundArray = CompoundArray, + DashCap = DashCap, + DashOffset = DashOffset, + DashPattern = DashPattern, + StartCap = StartCap, + EndCap = EndCap, + LineJoin = LineJoin, + MiterLimit = MiterLimit, + Transform = new Matrix(Transform.m_matrix) + }; #endregion From b79742f2d24ac077ee9c722f5717df8dee966086 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 12:14:05 -0300 Subject: [PATCH 051/217] add Pen unit test --- test/Common/PenUnitTest.cs | 130 +++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 test/Common/PenUnitTest.cs diff --git a/test/Common/PenUnitTest.cs b/test/Common/PenUnitTest.cs new file mode 100644 index 0000000..1083067 --- /dev/null +++ b/test/Common/PenUnitTest.cs @@ -0,0 +1,130 @@ +using GeneXus.Drawing.Drawing2D; + +namespace GeneXus.Drawing.Test; + +internal class PenFUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_Color() + { + var color = Color.Red; + + using var pen = new Pen(color); + Assert.Multiple(() => + { + Assert.That(pen.Color, Is.EqualTo(color)); + Assert.That(pen.Width, Is.EqualTo(1.0f)); + Assert.That(pen.Alignment, Is.EqualTo(PenAlignment.Center)); + Assert.That(pen.CompoundArray, Is.Empty); + Assert.That(pen.DashCap, Is.EqualTo(DashCap.Flat)); + Assert.That(pen.DashOffset, Is.EqualTo(0)); + Assert.That(pen.DashPattern, Is.Empty); + Assert.That(pen.PenType, Is.EqualTo(PenType.SolidColor)); + Assert.That(pen.StartCap, Is.EqualTo(LineCap.Flat)); + Assert.That(pen.EndCap, Is.EqualTo(LineCap.Flat)); + Assert.That(pen.LineJoin, Is.EqualTo(LineJoin.Miter)); + Assert.That(pen.MiterLimit, Is.EqualTo(10)); + Assert.That(pen.Transform.IsIdentity, Is.True); + }); + } + + [Test] + public void Constructor_ColorAndWidth() + { + var color = Color.Green; + var width = 5.0f; + + using var pen = new Pen(color, width); + Assert.Multiple(() => + { + Assert.That(pen.Color, Is.EqualTo(color)); + Assert.That(pen.Width, Is.EqualTo(width)); + }); + } + + [Test] + public void Constructor_Brush() + { + var color = Color.Green; + var brush = new SolidBrush(color); + var width = 5.0f; + + using var pen = new Pen(brush, width); + Assert.Multiple(() => + { + Assert.That(pen.Color, Is.EqualTo(color)); + Assert.That(pen.Width, Is.EqualTo(width)); + }); + } + + [Test] + public void Constructor_Properties() + { + var color = Color.Red; + var width = 10.0f; + var alignment = PenAlignment.Left; + var brush = new LinearGradientBrush(new Point(0, 0), new Point(1, 1), Color.Black, Color.White); + var compund = new[] { 1.0f, 2.0f }; + var dashCap = DashCap.Round; + var dashOffset = 3.0f; + var dashPattern = new[] { 0.0f, 5.0f, 10.0f }; + var lineCap1 = LineCap.Round; + var lineCap2 = LineCap.Square; + var lineJoin = LineJoin.Bevel; + var mitterLimit = 5.0f; + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + + using var pen = new Pen(Color.Green, 5.0f) + { + Color = color, + Width = width, + Alignment = alignment, + Brush = brush, + CompoundArray = compund, + DashCap = dashCap, + DashOffset = dashOffset, + DashPattern = dashPattern, + StartCap = lineCap1, + EndCap = lineCap2, + LineJoin = lineJoin, + MiterLimit = mitterLimit, + Transform = matrix, + }; + + Assert.Multiple(() => + { + Assert.That(pen.Color, Is.EqualTo(color)); + Assert.That(pen.Width, Is.EqualTo(width)); + Assert.That(pen.Alignment, Is.EqualTo(alignment)); + Assert.That(pen.Brush, Is.TypeOf()); + Assert.That(pen.PenType, Is.EqualTo(PenType.LinearGradient)); + Assert.That(pen.CompoundArray, Is.EqualTo(compund)); + Assert.That(pen.DashCap, Is.EqualTo(dashCap)); + Assert.That(pen.DashOffset, Is.EqualTo(dashOffset)); + Assert.That(pen.DashPattern, Is.EqualTo(dashPattern)); + Assert.That(pen.StartCap, Is.EqualTo(lineCap1)); + Assert.That(pen.EndCap, Is.EqualTo(lineCap2)); + Assert.That(pen.LineJoin, Is.EqualTo(lineJoin)); + Assert.That(pen.MiterLimit, Is.EqualTo(mitterLimit)); + Assert.That(pen.Transform, Is.EqualTo(matrix)); + }); + } + + [Test] + public void Method_Clone() + { + using var pen1 = new Pen(Color.Blue, 2.0f); + using var pen2 = pen1.Clone() as Pen; + Assert.Multiple(() => + { + Assert.That(pen2.Color, Is.EqualTo(pen1.Color)); + Assert.That(pen2.Width, Is.EqualTo(pen1.Width)); + Assert.That(pen2.Brush, Is.TypeOf()); + }); + } +} \ No newline at end of file From d1fa466104a1f129cc5f4d791bcfbefff634e8cc Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 12:22:15 -0300 Subject: [PATCH 052/217] minor change in PenUnitTest class --- test/Common/PenUnitTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Common/PenUnitTest.cs b/test/Common/PenUnitTest.cs index 1083067..f4c980a 100644 --- a/test/Common/PenUnitTest.cs +++ b/test/Common/PenUnitTest.cs @@ -2,7 +2,7 @@ namespace GeneXus.Drawing.Test; -internal class PenFUnitTest +internal class PenUnitTest { [SetUp] public void Setup() From 85f26d42b273b0ef2981df4ab10892e68c72359b Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 12:30:55 -0300 Subject: [PATCH 053/217] fix clone method unit test for several classes --- test/Common/Drawing2D/GraphicsPathUnitTest.cs | 3 ++- test/Common/Drawing2D/MatrixUnitTest.cs | 1 + test/Common/PenUnitTest.cs | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/Common/Drawing2D/GraphicsPathUnitTest.cs b/test/Common/Drawing2D/GraphicsPathUnitTest.cs index ba99721..fd8c24b 100644 --- a/test/Common/Drawing2D/GraphicsPathUnitTest.cs +++ b/test/Common/Drawing2D/GraphicsPathUnitTest.cs @@ -75,8 +75,9 @@ public void Method_Clone() var path2 = path1.Clone(); Assert.Multiple(() => { - Assert.That(path2, Is.TypeOf()); + Assert.That(path2, Is.Not.Null); Assert.That(path2, Is.Not.SameAs(path1)); + Assert.That(path2, Is.TypeOf()); if (path2 is GraphicsPath cloned) { diff --git a/test/Common/Drawing2D/MatrixUnitTest.cs b/test/Common/Drawing2D/MatrixUnitTest.cs index 1969b71..e6332ba 100644 --- a/test/Common/Drawing2D/MatrixUnitTest.cs +++ b/test/Common/Drawing2D/MatrixUnitTest.cs @@ -89,6 +89,7 @@ public void Method_Clone() Assert.Multiple(() => { Assert.That(clone, Is.Not.Null); + Assert.That(clone, Is.Not.SameAs(original)); Assert.That(clone, Is.EqualTo(original)); Assert.That(ReferenceEquals(clone, original), Is.False); Assert.That(clone.Elements, Is.EqualTo(original.Elements)); diff --git a/test/Common/PenUnitTest.cs b/test/Common/PenUnitTest.cs index f4c980a..055c2ac 100644 --- a/test/Common/PenUnitTest.cs +++ b/test/Common/PenUnitTest.cs @@ -122,6 +122,8 @@ public void Method_Clone() using var pen2 = pen1.Clone() as Pen; Assert.Multiple(() => { + Assert.That(pen1, Is.Not.Null); + Assert.That(pen1, Is.Not.SameAs(pen2)); Assert.That(pen2.Color, Is.EqualTo(pen1.Color)); Assert.That(pen2.Width, Is.EqualTo(pen1.Width)); Assert.That(pen2.Brush, Is.TypeOf()); From 2396f83bab86f8c205fcf27bce15d942420688a0 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 12:33:10 -0300 Subject: [PATCH 054/217] add SolidBrushUnitTest --- test/Common/SolidBrushUnitTest.cs | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/Common/SolidBrushUnitTest.cs diff --git a/test/Common/SolidBrushUnitTest.cs b/test/Common/SolidBrushUnitTest.cs new file mode 100644 index 0000000..f8d9cb5 --- /dev/null +++ b/test/Common/SolidBrushUnitTest.cs @@ -0,0 +1,41 @@ +namespace GeneXus.Drawing.Test; + +internal class SolidBrushUnitTest +{ + + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_Color() + { + var color = Color.Red; + + using var brush = new SolidBrush(color); + Assert.That(brush.Color, Is.EqualTo(color)); + } + + [Test] + public void Property_Color() + { + var color = Color.Blue; + + using var brush = new SolidBrush(Color.Red); + brush.Color = color; + Assert.That(brush.Color, Is.EqualTo(color)); + } + + [Test] + public void Method_Clone() + { + using var brush1 = new SolidBrush(Color.Blue); + using var brush2 = brush1.Clone() as SolidBrush; + Assert.Multiple(() => + { + Assert.That(brush2, Is.Not.SameAs(brush1)); + Assert.That(brush2.Color, Is.EqualTo(brush1.Color)); + }); + } +} \ No newline at end of file From 0de69a158dc3e1401600060e1552dc21fdc2a057 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 13:35:31 -0300 Subject: [PATCH 055/217] minor change --- test/Common/Drawing2D/MatrixUnitTest.cs | 1 - test/Common/SolidBrushUnitTest.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/test/Common/Drawing2D/MatrixUnitTest.cs b/test/Common/Drawing2D/MatrixUnitTest.cs index e6332ba..6ec04e1 100644 --- a/test/Common/Drawing2D/MatrixUnitTest.cs +++ b/test/Common/Drawing2D/MatrixUnitTest.cs @@ -5,7 +5,6 @@ namespace GeneXus.Drawing.Test.Drawing2D; internal class MatrixTests { - private const float TOLERANCE = 0.001f; [SetUp] diff --git a/test/Common/SolidBrushUnitTest.cs b/test/Common/SolidBrushUnitTest.cs index f8d9cb5..76c4068 100644 --- a/test/Common/SolidBrushUnitTest.cs +++ b/test/Common/SolidBrushUnitTest.cs @@ -2,7 +2,6 @@ namespace GeneXus.Drawing.Test; internal class SolidBrushUnitTest { - [SetUp] public void Setup() { From ef69797f5abccd376e977d253821b630b1afc304 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:07:34 -0300 Subject: [PATCH 056/217] update TestureBrush for supporing RectangleF and Image classes, and dynamically update shader --- src/Common/TextureBrush.cs | 77 +++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/src/Common/TextureBrush.cs b/src/Common/TextureBrush.cs index fa4e339..e96043a 100644 --- a/src/Common/TextureBrush.cs +++ b/src/Common/TextureBrush.cs @@ -6,38 +6,54 @@ namespace GeneXus.Drawing; public sealed class TextureBrush : Brush { - internal Bitmap m_bitmap; - internal Rectangle m_rect; + internal Image m_image; + internal RectangleF m_bounds; internal WrapMode m_mode; - private TextureBrush(Rectangle rect, Bitmap bitmap, WrapMode mode) - : base(new SKPaint { Shader = CreateShader(bitmap, rect, mode) }) + private TextureBrush(RectangleF rect, Image image, WrapMode mode) + : base(new SKPaint { }) { - m_rect = rect; - m_bitmap = bitmap; + m_bounds = rect; + m_image = image; m_mode = mode; + + UpdateShader(() => { }); } /// /// Initializes a new object that uses the /// specified image, wrap mode, and bounding rectangle. /// - public TextureBrush(Bitmap bitmap, Rectangle rect, WrapMode mode = WrapMode.Tile) - : this(rect, bitmap, mode) { } + public TextureBrush(Image image, RectangleF rect, WrapMode mode = WrapMode.Tile) + : this(rect, image, mode) { } + + /// + /// Initializes a new object that uses the + /// specified image, wrap mode, and bounding rectangle. + /// + public TextureBrush(Image image, Rectangle rect, WrapMode mode = WrapMode.Tile) + : this(new RectangleF(rect.m_rect), image, mode) { } /// /// Initializes a new object that uses the /// specified image and wrap mode. /// - public TextureBrush(Bitmap bitmap, WrapMode mode = WrapMode.Tile) - : this(bitmap, new Rectangle(0, 0, bitmap.Size), mode) { } + public TextureBrush(Image image, WrapMode mode = WrapMode.Tile) + : this(image, new Rectangle(0, 0, image.Size), mode) { } /// /// Initializes a new object that uses /// the specified image, bounding rectangle, and image attributes. /// - public TextureBrush(Bitmap bitmap, Rectangle rect, object imageAttributes) - : this(bitmap, rect, WrapMode.Tile) { } // TODO: implement ImageAttributes class + public TextureBrush(Image image, RectangleF rect, object imageAttributes) + : this(image, rect, WrapMode.Tile) { } // TODO: implement ImageAttributes class + + /// + /// Initializes a new object that uses + /// the specified image, bounding rectangle, and image attributes. + /// + public TextureBrush(Image image, Rectangle rect, object imageAttributes) + : this(image, new RectangleF(rect.m_rect), imageAttributes) { } #region IClonable @@ -46,7 +62,7 @@ public TextureBrush(Bitmap bitmap, Rectangle rect, object imageAttributes) /// Creates an exact copy of this . /// public override object Clone() - => new TextureBrush(m_rect, m_bitmap, m_mode); + => new TextureBrush(m_bounds, m_image, m_mode); #endregion @@ -57,7 +73,7 @@ public override object Clone() /// Gets the object associated with /// this object. /// - public Image Image => m_bitmap; + public Image Image => m_image; /// /// Gets or sets a copy of the object @@ -73,7 +89,7 @@ public override object Clone() public WrapMode WrapMode { get => m_mode; - set => m_mode = value; // TODO: update shader when updating this + set => UpdateShader(() => m_mode = value); } #endregion @@ -121,15 +137,11 @@ public void TranslateTransform(float dx, float dy, MatrixOrder order) #region Utilities - private static SKShader CreateShader(Bitmap bitmap, Rectangle rect, WrapMode mode) + private void UpdateShader(Action action) { - /* - * NOTE: For mapping WrapMode.Clamp - * - SKShaderTileMode.Clamp: Replicate the edge color if the shader draws outside of its original bounds - * - SKShaderTileMode.Decal: Only draw within the original domain, return transparent-black everywhere else. - */ - - (var tmx, var tmy) = mode switch + action(); + + (var tmx, var tmy) = WrapMode switch { WrapMode.Tile => (SKShaderTileMode.Repeat, SKShaderTileMode.Repeat), WrapMode.Clamp => (SKShaderTileMode.Decal, SKShaderTileMode.Decal), @@ -139,13 +151,26 @@ private static SKShader CreateShader(Bitmap bitmap, Rectangle rect, WrapMode mod _ => throw new NotImplementedException() }; - var info = new SKImageInfo((int)rect.Width, (int)rect.Height); + var info = new SKImageInfo((int)m_bounds.Width, (int)m_bounds.Height); using var surfece = SKSurface.Create(info); - surfece.Canvas.DrawBitmap(bitmap.m_bitmap, rect.m_rect); + switch (m_image) + { + case Bitmap bitmap: + surfece.Canvas.DrawBitmap(bitmap.m_bitmap, m_bounds.m_rect); + break; + + case Svg svg: + surfece.Canvas.DrawImage(svg.InnerImage, m_bounds.m_rect); + break; + + default: + throw new NotImplementedException($"image type {m_image.GetType().Name}."); + } var src = surfece.Snapshot(); - return SKShader.CreateImage(src, tmx, tmy); + + m_paint.Shader = SKShader.CreateImage(src, tmx, tmy); } #endregion From f2ba40888f6a03ac34fbf2cdc88741c53466c748 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:12:41 -0300 Subject: [PATCH 057/217] add HatchBrush constructor summary and missing constructor (without background color) --- src/Common/Drawing2D/HatchBrush.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Common/Drawing2D/HatchBrush.cs b/src/Common/Drawing2D/HatchBrush.cs index eccdb96..f1e971a 100644 --- a/src/Common/Drawing2D/HatchBrush.cs +++ b/src/Common/Drawing2D/HatchBrush.cs @@ -9,14 +9,27 @@ public sealed class HatchBrush : Brush private readonly Color m_back; private readonly HatchStyle m_style; + /// + /// Initializes a new instance of the class with the + /// specified enumeration, foreground color, and background color. + /// public HatchBrush(HatchStyle hatchStyle, Color foreColor, Color backColor) : base(new SKPaint { Shader = CreateShader(hatchStyle, foreColor, backColor) }) { m_fore = foreColor; m_back = backColor; m_style = hatchStyle; + + UpdateShader(() => { }); } + /// + /// Initializes a new instance of the class with the + /// specified enumeration and foreground color. + /// + public HatchBrush(HatchStyle hatchStyle, Color foreColor) + : this(hatchStyle, foreColor, Color.Transparent) { } + #region IClonable /// From 3ae755c551e2938058e5a527fb0df424296e6181 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:13:23 -0300 Subject: [PATCH 058/217] change CreateShader by UpdateShader in HatchBrush class --- src/Common/Drawing2D/HatchBrush.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Common/Drawing2D/HatchBrush.cs b/src/Common/Drawing2D/HatchBrush.cs index f1e971a..abd3338 100644 --- a/src/Common/Drawing2D/HatchBrush.cs +++ b/src/Common/Drawing2D/HatchBrush.cs @@ -14,7 +14,7 @@ public sealed class HatchBrush : Brush /// specified enumeration, foreground color, and background color. /// public HatchBrush(HatchStyle hatchStyle, Color foreColor, Color backColor) - : base(new SKPaint { Shader = CreateShader(hatchStyle, foreColor, backColor) }) + : base(new SKPaint { }) { m_fore = foreColor; m_back = backColor; @@ -63,23 +63,25 @@ public override object Clone() #region Utilities - private static SKShader CreateShader(HatchStyle style, Color foreColor, Color backColor) + private void UpdateShader(Action action) { + action(); + var size = 10f; using var pattern = new SKBitmap((int)size, (int)size); using var canvas = new SKCanvas(pattern); - canvas.Clear(backColor.m_color); + canvas.Clear(m_back.m_color); var paint = new SKPaint { - Color = foreColor.m_color, + Color = m_fore.m_color, Style = SKPaintStyle.Stroke, StrokeWidth = 1, IsAntialias = true }; - switch (style) + switch (m_style) { case HatchStyle.Horizontal: canvas.DrawLine(0, size / 2, size, size / 2, paint); @@ -254,11 +256,11 @@ private static SKShader CreateShader(HatchStyle style, Color foreColor, Color ba break; case HatchStyle.SmallCheckerBoard: - DrawCheckerBoardPattern(canvas, paint, foreColor.m_color, backColor.m_color, size, 2); + DrawCheckerBoardPattern(canvas, paint, m_fore.m_color, m_back.m_color, size, 2); break; case HatchStyle.LargeCheckerBoard: - DrawCheckerBoardPattern(canvas, paint, foreColor.m_color, backColor.m_color, size, 4); + DrawCheckerBoardPattern(canvas, paint, m_fore.m_color, m_back.m_color, size, 4); break; case HatchStyle.Percent05: @@ -311,10 +313,10 @@ private static SKShader CreateShader(HatchStyle style, Color foreColor, Color ba // TODO: add other styles default: - throw new NotSupportedException($"Hatch style {style} is not supported."); + throw new NotSupportedException($"Hatch style {m_style} is not supported."); } - return SKShader.CreateBitmap(pattern, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); + m_paint.Shader = SKShader.CreateBitmap(pattern, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); } private static void DrawPercentagePattern(SKCanvas canvas, SKPaint paint, float size, int percent) From da393215723ee3b4cfede670b1b30326b72f7d2d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:16:47 -0300 Subject: [PATCH 059/217] add TextureBrushUnitTest --- test/Common/TextureBrushUnitTest.cs | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 test/Common/TextureBrushUnitTest.cs diff --git a/test/Common/TextureBrushUnitTest.cs b/test/Common/TextureBrushUnitTest.cs new file mode 100644 index 0000000..d8b76a0 --- /dev/null +++ b/test/Common/TextureBrushUnitTest.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using GeneXus.Drawing.Drawing2D; + +namespace GeneXus.Drawing.Test; + +internal class TextureBrushUnitTest +{ + private static readonly string IMAGE_PATH = Path.Combine( + Directory.GetParent(Environment.CurrentDirectory).Parent.FullName, + "res", "images"); + + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_BitmapRectWrap() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); + using var bitmap = new Bitmap(filePath); + var rect = new Rectangle(0, 0, 100, 50); + var wrapMode = WrapMode.Tile; + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + + using var brush = new TextureBrush(bitmap, rect, wrapMode) { Transform = matrix }; + Assert.Multiple(() => + { + Assert.That(brush.Image, Is.EqualTo(bitmap)); + Assert.That(brush.WrapMode, Is.EqualTo(wrapMode)); + Assert.That(brush.Transform, Is.EqualTo(matrix)); + }); + } +} \ No newline at end of file From c73fe322379981ce9ba802074808cede5d580f86 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:19:54 -0300 Subject: [PATCH 060/217] change indentation by tabs in Pen class --- src/Common/Pen.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Common/Pen.cs b/src/Common/Pen.cs index 95bcec5..bbbe173 100644 --- a/src/Common/Pen.cs +++ b/src/Common/Pen.cs @@ -74,8 +74,8 @@ public void Dispose() public object Clone() => new Pen(Color, Width) { - Alignment = Alignment, - Brush = Brush switch + Alignment = Alignment, + Brush = Brush switch { SolidBrush sb => sb.Clone() as SolidBrush, HatchBrush hb => hb.Clone() as HatchBrush, @@ -84,15 +84,15 @@ public object Clone() LinearGradientBrush lgb => lgb.Clone() as LinearGradientBrush, _ => throw new NotImplementedException($"undefined map to {Brush.GetType().Name}.") }, - CompoundArray = CompoundArray, - DashCap = DashCap, - DashOffset = DashOffset, - DashPattern = DashPattern, - StartCap = StartCap, - EndCap = EndCap, - LineJoin = LineJoin, - MiterLimit = MiterLimit, - Transform = new Matrix(Transform.m_matrix) + CompoundArray = CompoundArray, + DashCap = DashCap, + DashOffset = DashOffset, + DashPattern = DashPattern, + StartCap = StartCap, + EndCap = EndCap, + LineJoin = LineJoin, + MiterLimit = MiterLimit, + Transform = new Matrix(Transform.m_matrix) }; #endregion From 64cca1c1de406727f6aac489766a853fc3799451 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:28:32 -0300 Subject: [PATCH 061/217] fix TextureBrush.Clone method --- src/Common/TextureBrush.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Common/TextureBrush.cs b/src/Common/TextureBrush.cs index e96043a..e2f82ef 100644 --- a/src/Common/TextureBrush.cs +++ b/src/Common/TextureBrush.cs @@ -62,7 +62,10 @@ public TextureBrush(Image image, Rectangle rect, object imageAttributes) /// Creates an exact copy of this . /// public override object Clone() - => new TextureBrush(m_bounds, m_image, m_mode); + => new TextureBrush(m_bounds, m_image, m_mode) + { + Transform = Transform.Clone() as Matrix + }; #endregion From e04e3f27b37170e976a3b71b0f589f1467c1ea84 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:31:47 -0300 Subject: [PATCH 062/217] minor change in SolidBrushUnitTest --- test/Common/SolidBrushUnitTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Common/SolidBrushUnitTest.cs b/test/Common/SolidBrushUnitTest.cs index 76c4068..6c2e889 100644 --- a/test/Common/SolidBrushUnitTest.cs +++ b/test/Common/SolidBrushUnitTest.cs @@ -33,6 +33,7 @@ public void Method_Clone() using var brush2 = brush1.Clone() as SolidBrush; Assert.Multiple(() => { + Assert.That(brush2, Is.Not.Null); Assert.That(brush2, Is.Not.SameAs(brush1)); Assert.That(brush2.Color, Is.EqualTo(brush1.Color)); }); From 9b86273991666fb8002db822cf4ab6201b56489a Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:38:23 -0300 Subject: [PATCH 063/217] add unit test for Clone method in TextureBrushUnitTest --- test/Common/TextureBrushUnitTest.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/Common/TextureBrushUnitTest.cs b/test/Common/TextureBrushUnitTest.cs index d8b76a0..6e9eb8d 100644 --- a/test/Common/TextureBrushUnitTest.cs +++ b/test/Common/TextureBrushUnitTest.cs @@ -32,4 +32,22 @@ public void Constructor_BitmapRectWrap() Assert.That(brush.Transform, Is.EqualTo(matrix)); }); } + + [Test] + public void Method_Clone() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); + using var bitmap = new Bitmap(filePath); + + using var brush1 = new TextureBrush(bitmap, new Rectangle(0, 0, 100, 50), WrapMode.Tile) { Transform = new Matrix(1, 2, 3, 4, 5, 6) }; + using var brush2 = brush1.Clone() as TextureBrush; + Assert.Multiple(() => + { + Assert.That(brush2, Is.Not.Null); + Assert.That(brush2, Is.Not.SameAs(brush1)); + Assert.That(brush2.Image, Is.EqualTo(brush1.Image)); + Assert.That(brush2.WrapMode, Is.EqualTo(brush1.WrapMode)); + Assert.That(brush2.Transform, Is.EqualTo(brush1.Transform)); + }); + } } \ No newline at end of file From 6ae48f15ecf205f745aa00b2e21c7acb66670200 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:40:19 -0300 Subject: [PATCH 064/217] add HatchBrushUnitTest --- test/Common/Drawing2D/HatchBrushUnitTest.cs | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 test/Common/Drawing2D/HatchBrushUnitTest.cs diff --git a/test/Common/Drawing2D/HatchBrushUnitTest.cs b/test/Common/Drawing2D/HatchBrushUnitTest.cs new file mode 100644 index 0000000..972453f --- /dev/null +++ b/test/Common/Drawing2D/HatchBrushUnitTest.cs @@ -0,0 +1,42 @@ +using GeneXus.Drawing.Drawing2D; + +namespace GeneXus.Drawing.Test.Drawing2D; + +internal class HatchBrushUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_StyleForeBack() + { + var style = HatchStyle.Horizontal; + var back = Color.Red; + var fore = Color.Blue; + + using var brush = new HatchBrush(style, fore, back); + Assert.Multiple(() => + { + Assert.That(brush.BackgroundColor, Is.EqualTo(back)); + Assert.That(brush.ForegroundColor, Is.EqualTo(fore)); + Assert.That(brush.HatchStyle, Is.EqualTo(style)); + }); + } + + [Test] + public void Method_Clone() + { + using var brush1 = new HatchBrush(HatchStyle.Vertical, Color.Red, Color.Blue); + using var brush2 = brush1.Clone() as HatchBrush; + Assert.Multiple(() => + { + Assert.That(brush2, Is.Not.Null); + Assert.That(brush2, Is.Not.SameAs(brush1)); + Assert.That(brush2.HatchStyle, Is.EqualTo(brush1.HatchStyle)); + Assert.That(brush2.ForegroundColor, Is.EqualTo(brush1.ForegroundColor)); + Assert.That(brush2.BackgroundColor, Is.EqualTo(brush1.BackgroundColor)); + }); + } +} \ No newline at end of file From 250bb7fc1ec85f44dcdf1eeafaf9cdb9d5009b2c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 25 Jul 2024 14:44:18 -0300 Subject: [PATCH 065/217] minor change --- test/Common/Drawing2D/HatchBrushUnitTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Common/Drawing2D/HatchBrushUnitTest.cs b/test/Common/Drawing2D/HatchBrushUnitTest.cs index 972453f..340634a 100644 --- a/test/Common/Drawing2D/HatchBrushUnitTest.cs +++ b/test/Common/Drawing2D/HatchBrushUnitTest.cs @@ -23,7 +23,7 @@ public void Constructor_StyleForeBack() Assert.That(brush.ForegroundColor, Is.EqualTo(fore)); Assert.That(brush.HatchStyle, Is.EqualTo(style)); }); - } + } [Test] public void Method_Clone() From 4f4398357678b6096a24732b7deee1236cd0ca0f Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 26 Jul 2024 13:46:18 -0300 Subject: [PATCH 066/217] add validation to InterpolationColors in LinearGradientBrush --- src/Common/Drawing2D/LinearGradientBrush.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 9dfe86b..118b705 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -118,7 +118,14 @@ public bool GammaCorrection public ColorBlend InterpolationColors { get => m_colors; - set => UpdateShader(() => m_colors = value); + set => UpdateShader(() => + { + if (value.Positions[0] != 0 ) + throw new ArgumentException("first element must be equal to 0.", nameof(value)); + if (value.Positions[value.Positions.Length - 1] != 1) + throw new ArgumentException("last element must be equal to 1.", nameof(value)); + m_colors = value; + }); } /// From 63d1ca7e5af7b1fcfa89b369ffac254e4bab7c31 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 26 Jul 2024 15:44:45 -0300 Subject: [PATCH 067/217] fix LinearGradientBrush constructor --- src/Common/Drawing2D/LinearGradientBrush.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 118b705..4812725 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -22,12 +22,16 @@ private LinearGradientBrush(GradientVector vector, Color[] colors, WrapMode mode m_transform = transform; m_gamma = false; - m_blend = null; - m_colors = new() - { - Colors = colors, - Positions = CreateUniformArray(colors.Length) - }; + + m_blend = new(); + Array.Copy(new[] { 1f }, m_blend.Factors, 1); + Array.Copy(new[] { 0f }, m_blend.Positions, 1); + + var uniform = CreateUniformArray(colors.Length); + + m_colors = new(colors.Length); + Array.Copy(colors, m_colors.Colors, colors.Length); + Array.Copy(uniform, m_colors.Positions, colors.Length); UpdateShader(() => { }); } From b1d62a00925c203e5a011190bcd2bce0d9c397ce Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 26 Jul 2024 15:45:26 -0300 Subject: [PATCH 068/217] check null in InterpolationColors of LinearGradientBrush --- src/Common/Drawing2D/LinearGradientBrush.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 4812725..b3b2efd 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -124,11 +124,12 @@ public ColorBlend InterpolationColors get => m_colors; set => UpdateShader(() => { - if (value.Positions[0] != 0 ) + var colors = value ?? throw new ArgumentNullException(nameof(value)); + if (colors.Positions[0] != 0 ) throw new ArgumentException("first element must be equal to 0.", nameof(value)); - if (value.Positions[value.Positions.Length - 1] != 1) + if (colors.Positions[value.Positions.Length - 1] != 1) throw new ArgumentException("last element must be equal to 1.", nameof(value)); - m_colors = value; + m_colors = colors; }); } From 6b3f43ebd700c9310c860779a559d6e0e7aa578b Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 26 Jul 2024 15:46:32 -0300 Subject: [PATCH 069/217] fix UpdateShader of LinearGradientBrush based on factor and color blends --- src/Common/Drawing2D/LinearGradientBrush.cs | 52 +++++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index b3b2efd..05095c9 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -296,20 +296,54 @@ private void UpdateShader(Action action) } var vector = GetGradientVector(m_rect, LinearGradientMode.ForwardDiagonal); + var gamma = m_gamma ? 2.2f : 1.0f; + var start = vector.BegPoint.m_point; var end = vector.EndPoint.m_point; - var gamma = m_gamma ? 2.2f : 1.0f; - var factors = m_blend?.Factors ?? Enumerable.Repeat(1f, m_colors.Positions.Length).ToArray(); - var positions = m_colors.Positions - .Take(factors.Length) - .ToArray(); - var colors = m_colors.Colors - .Zip(factors, ApplyFactor) - .Select(color => ApplyGamma(color, gamma).m_color) - .ToArray(); var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; var matrix = m_transform.m_matrix; + int index; + + var blend = new Dictionary(); + for (index = 0; index < m_blend.Positions.Length; index++) + { + var pos = m_blend.Positions[index]; + var fac = m_blend.Factors[index]; + blend[pos] = fac; // blend factor + } + for (index = 0; index < m_colors.Positions.Length; index++) + { + var pos = m_colors.Positions[index]; + var col = m_colors.Colors[index]; + blend[pos] = col; // specific color + } + + var lastColor = Color.Empty; + var blendKeys = blend.Keys.OrderBy(key => key); + + var positions = blendKeys.ToArray(); + var colors = new SKColor[positions.Length]; + + index = 0; + foreach (var key in blendKeys) + { + var value = blend[key]; + if (value is Color currColor) + { + var color = ApplyGamma(currColor, gamma); + colors[index++] = color.m_color; + lastColor = currColor; + continue; + } + if (value is float factor) + { + var color = ApplyFactor(lastColor, factor); + colors[index++] = color.m_color; + continue; + } + } + m_paint.Shader = SKShader.CreateLinearGradient(start, end, colors, positions, mode, matrix); } From 728699413bb18f7188668081f5fb940c2b926fcb Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 26 Jul 2024 15:47:49 -0300 Subject: [PATCH 070/217] implement SetSigmaBellShape and SetBlendTriangularShape methdos of LinearGradientBrush --- src/Common/Drawing2D/LinearGradientBrush.cs | 136 ++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 05095c9..87faba2 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using SkiaSharp; @@ -216,6 +217,104 @@ public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.P public void TranslateTransform(float dx, float dy, MatrixOrder order) => UpdateShader(() => Transform.Translate(dx, dy, order)); + /// + /// Creates a gradient falloff based on a bell-shaped curve. + /// + public void SetSigmaBellShape(float focus, float scale = 1.0f) + => UpdateShader(() => + { + if (focus < 0 || focus > 1) + throw new ArgumentException("Invalid focus value", nameof(focus)); + if (scale < 0 || scale > 1) + throw new ArgumentException("Invalid scale value", nameof(scale)); + + int count = focus == 0 || focus == 1 ? 256 : 511; + m_blend = new(count); + + // TODO: clear preset colors + + float fallOffLenght = 2.0f; + if (focus == 0) + { + m_blend.Positions[0] = focus; + m_blend.Factors[0] = scale; + + SigmaBellBlend(focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, true); + + m_blend.Positions[count - 1] = 1f; + m_blend.Factors[count - 1] = 0f; + } + else if (focus == 1) + { + m_blend.Positions[0] = 0f; + m_blend.Factors[0] = 0f; + + SigmaBellBlend(focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, false); + + m_blend.Positions[count - 1] = focus; + m_blend.Factors[count - 1] = scale; + } + else + { + int middle = count / 2; + + // left part of the sigma bell + m_blend.Positions[0] = 0f; + m_blend.Factors[0] = 0f; + + SigmaBellBlend(focus, scale, focus / (2 * fallOffLenght), focus / 2, focus / 255, 1, middle - 1, false); + + // middle part of the sigma bell + m_blend.Positions[middle - 1] = focus; + m_blend.Factors[middle - 1] = scale; + + // right part of the sigma bell + SigmaBellBlend(focus, scale, (1 - focus) / (2 * fallOffLenght), (1 + focus) / 2, (1 - focus) / 255, middle, count - 1, true); + + m_blend.Positions[count - 1] = 1f; + m_blend.Factors[count - 1] = 0f; + } + }); + + /// + /// Creates a linear gradient with a center color and a linear falloff to a single color on both ends. + /// + public void SetBlendTriangularShape(float focus, float scale = 1.0f) + => UpdateShader(() => + { + if (focus < 0 || focus > 1) + throw new ArgumentException("Invalid focus value", nameof(focus)); + if (scale < 0 || scale > 1) + throw new ArgumentException("Invalid scale value", nameof(scale)); + + int count = focus == 0 || focus == 1 ? 2 : 3; + m_blend = new(count); + + if (focus == 0) + { + m_blend.Positions[0] = 0; + m_blend.Factors[1] = scale; + m_blend.Positions[1] = 1; + m_blend.Factors[1] = 0; + } + else if (focus == 1) + { + m_blend.Positions[0] = 0; + m_blend.Factors[1] = 0; + m_blend.Positions[1] = 1; + m_blend.Factors[1] = scale; + } + else + { + m_blend.Positions[0] = 0; + m_blend.Factors[0] = 0; + m_blend.Positions[1] = focus; + m_blend.Factors[1] = scale; + m_blend.Positions[2] = 1; + m_blend.Factors[2] = 0; + } + }); + #endregion @@ -278,6 +377,43 @@ private static float[] CreateUniformArray(int length) ? throw new ArgumentException("at least two items are required.", nameof(length)) : Enumerable.Range(0, length).Select(i => 1f * i / (length - 1)).ToArray(); + void SigmaBellBlend(float focus, float scale, float sigma, float mean, float delta, int startIndex, int endIndex, bool invert) + { + float cb = (1 - Erf(invert ? 1f : 0f, sigma, mean)) / 2; + float ct = (1 - Erf(focus, sigma, mean)) / 2; + float ch = ct - cb; + float pos = delta; + + for (int index = startIndex; index < endIndex; index++) + { + m_blend.Positions[index] = pos; + m_blend.Factors[index] = scale * ((1 - Erf(pos, sigma, mean)) / 2 - cb) / ch; + pos += delta; + } + + static float Erf(float x, float sigma, float mean, int terms = 6) + { + /* + * Error function (Erf) for Gaussian distribution by Maclaurin series: + * erf (z) = (2 / sqrt (pi)) * infinite sum of [(pow (-1, n) * pow (z, 2n+1))/(n! * (2n+1))] + */ + float constant = 2 / (float)Math.Sqrt(Math.PI); + float z = (x - mean) / (sigma * (float)Math.Sqrt(2)); + + float series = z; + + float z2 = z * z; + float zn = z2; + for (int n = 1, fact = 1; n < terms; n++, zn *= -z2, fact *= n) + { + float term = -z * zn / (fact * (2 * n + 1)); + series += term; + } + + return constant * series; + } + } + private void UpdateShader(Action action) { action(); From 618ff2da8484d78c6f35c105f280a323f40fce6f Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 26 Jul 2024 19:22:40 -0300 Subject: [PATCH 071/217] fix SigmaBellBlend helper in LinearGradientBrush --- src/Common/Drawing2D/LinearGradientBrush.cs | 40 +++++++++++---------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 87faba2..e1a3cd1 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -262,14 +262,14 @@ public void SetSigmaBellShape(float focus, float scale = 1.0f) m_blend.Positions[0] = 0f; m_blend.Factors[0] = 0f; - SigmaBellBlend(focus, scale, focus / (2 * fallOffLenght), focus / 2, focus / 255, 1, middle - 1, false); + SigmaBellBlend(focus, scale, focus / (2 * fallOffLenght), focus / 2, focus / 255, 1, middle, false); // middle part of the sigma bell - m_blend.Positions[middle - 1] = focus; - m_blend.Factors[middle - 1] = scale; + m_blend.Positions[middle] = focus; + m_blend.Factors[middle] = scale; // right part of the sigma bell - SigmaBellBlend(focus, scale, (1 - focus) / (2 * fallOffLenght), (1 + focus) / 2, (1 - focus) / 255, middle, count - 1, true); + SigmaBellBlend(focus, scale, (1 - focus) / (2 * fallOffLenght), (1 + focus) / 2, (1 - focus) / 255, middle + 1, count - 1, true); m_blend.Positions[count - 1] = 1f; m_blend.Factors[count - 1] = 0f; @@ -379,35 +379,37 @@ private static float[] CreateUniformArray(int length) void SigmaBellBlend(float focus, float scale, float sigma, float mean, float delta, int startIndex, int endIndex, bool invert) { - float cb = (1 - Erf(invert ? 1f : 0f, sigma, mean)) / 2; - float ct = (1 - Erf(focus, sigma, mean)) / 2; + float sg = invert ? -1 : 1; + float x0 = invert ? 1f : 0f; + + float cb = (1 + sg * Erf(x0, sigma, mean)) / 2; + float ct = (1 + sg * Erf(focus, sigma, mean)) / 2; float ch = ct - cb; - float pos = delta; - for (int index = startIndex; index < endIndex; index++) + float offset = invert ? focus : 0; + float pos = delta + offset; + + for (int index = startIndex; index < endIndex; index++, pos += delta) { m_blend.Positions[index] = pos; - m_blend.Factors[index] = scale * ((1 - Erf(pos, sigma, mean)) / 2 - cb) / ch; - pos += delta; + m_blend.Factors[index] = scale / ch * ((1 + sg * Erf(pos, sigma, mean)) / 2 - cb); } static float Erf(float x, float sigma, float mean, int terms = 6) { /* - * Error function (Erf) for Gaussian distribution by Maclaurin series: - * erf (z) = (2 / sqrt (pi)) * infinite sum of [(pow (-1, n) * pow (z, 2n+1))/(n! * (2n+1))] - */ + * Error function (Erf) for Gaussian distribution by Maclaurin series: + * erf (z) = (2 / sqrt (pi)) * infinite sum of [(pow (-1, n) * pow (z, 2n+1))/(n! * (2n+1))] + */ float constant = 2 / (float)Math.Sqrt(Math.PI); float z = (x - mean) / (sigma * (float)Math.Sqrt(2)); float series = z; - - float z2 = z * z; - float zn = z2; - for (int n = 1, fact = 1; n < terms; n++, zn *= -z2, fact *= n) + for (int n = 1, fact = 1; n < terms; n++, fact *= n) { - float term = -z * zn / (fact * (2 * n + 1)); - series += term; + int sign = (int)Math.Pow(-1, n); + int step = 2 * n + 1; + series += sign * (float)Math.Pow(z, step) / (fact * step); } return constant * series; From 2a967341bf863d2d2e2df7ead92534d23b278d26 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 29 Jul 2024 01:25:53 -0300 Subject: [PATCH 072/217] fix LinearGradientBrush class --- src/Common/Drawing2D/LinearGradientBrush.cs | 194 ++++++++++++-------- 1 file changed, 119 insertions(+), 75 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index e1a3cd1..cf34061 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -15,10 +15,10 @@ public sealed class LinearGradientBrush : Brush private ColorBlend m_colors; private bool m_gamma; - private LinearGradientBrush(GradientVector vector, Color[] colors, WrapMode mode, Matrix transform) + private LinearGradientBrush(RectangleF rect, Color[] colors, WrapMode mode, Matrix transform) : base(new SKPaint { }) { - m_rect = new RectangleF(vector.BegPoint.X, vector.BegPoint.Y, vector.BegPoint.X + vector.EndPoint.X, vector.BegPoint.Y + vector.EndPoint.Y); + m_rect = rect; m_mode = mode; m_transform = transform; @@ -27,8 +27,8 @@ private LinearGradientBrush(GradientVector vector, Color[] colors, WrapMode mode m_blend = new(); Array.Copy(new[] { 1f }, m_blend.Factors, 1); Array.Copy(new[] { 0f }, m_blend.Positions, 1); - - var uniform = CreateUniformArray(colors.Length); + + var uniform = GetUniformArray(colors.Length); m_colors = new(colors.Length); Array.Copy(colors, m_colors.Colors, colors.Length); @@ -42,7 +42,7 @@ private LinearGradientBrush(GradientVector vector, Color[] colors, WrapMode mode /// points and colors. /// public LinearGradientBrush(PointF point1, PointF point2, Color color1, Color color2) - : this(GetGradientVector(point1, point2), new[] { color1, color2 }, WrapMode.Tile, new Matrix()) { } + : this(GetRectangle(point1, point2), color1, color2, 0, false) { } /// /// Initializes a new instance of the class with the specified @@ -56,7 +56,7 @@ public LinearGradientBrush(Point point1, Point point2, Color color1, Color color /// a , starting and ending colors, and orientation. /// public LinearGradientBrush(RectangleF rect, Color color1, Color color2, LinearGradientMode mode) - : this(GetGradientVector(rect, mode), new[] { color1, color2 }, WrapMode.Tile, new Matrix()) { } + : this(rect, color1, color2, GetAngle(mode), false) { } /// /// Creates a new instance of the class based on @@ -70,7 +70,7 @@ public LinearGradientBrush(Rectangle rect, Color color1, Color color2, LinearGra /// a , starting and ending colors, and an orientation angle. /// public LinearGradientBrush(RectangleF rect, Color color1, Color color2, float angle, bool isAngleScaleable = false) - : this(GetGradientVector(rect, angle, isAngleScaleable), new[] { color1, color2 }, WrapMode.Tile, new Matrix()) { } + : this(rect, new[] { color1, color2 }, WrapMode.Tile, GetTransform(rect, angle, isAngleScaleable)) { } /// /// Creates a new instance of the class based on @@ -85,11 +85,13 @@ public LinearGradientBrush(Rectangle rect, Color color1, Color color2, float ang /// /// Creates an exact copy of this . /// - public override object Clone() - { - var vector = GetGradientVector(m_rect, LinearGradientMode.ForwardDiagonal); - return new LinearGradientBrush(vector, m_colors.Colors, m_mode, m_transform); - } + public override object Clone() + => new LinearGradientBrush(Rectangle, LinearColors, WrapMode, Transform) + { + Blend = Blend, + InterpolationColors = InterpolationColors, + GammaCorrection = GammaCorrection + }; #endregion @@ -143,7 +145,7 @@ public Color[] LinearColors set => UpdateShader(() => m_colors = new() { Colors = value, - Positions = CreateUniformArray(value.Length) + Positions = GetUniformArray(value.Length) }); } @@ -320,62 +322,100 @@ public void SetBlendTriangularShape(float focus, float scale = 1.0f) #region Utilities - private struct GradientVector - { - public PointF BegPoint { get; internal set; } - public PointF EndPoint { get; internal set; } - } - - private static GradientVector GetGradientVector(PointF start, PointF end) - => new() { BegPoint = start, EndPoint = end }; + private static RectangleF GetRectangle(PointF point1, PointF point2) + => new(point1.X, point1.Y, point1.X + point2.X, point1.Y + point2.Y); - private static GradientVector GetGradientVector(RectangleF rect, LinearGradientMode mode) + private static float GetAngle(LinearGradientMode mode) => mode switch { - (var begPoint, var endPoint) = mode switch - { - LinearGradientMode.Horizontal => ( new PointF(rect.Left, rect.Top), new PointF(rect.Right, rect.Top) ), - LinearGradientMode.Vertical => ( new PointF(rect.Left, rect.Top), new PointF(rect.Left, rect.Bottom) ), - LinearGradientMode.ForwardDiagonal => ( new PointF(rect.Left, rect.Top), new PointF(rect.Right, rect.Bottom) ), - LinearGradientMode.BackwardDiagonal => ( new PointF(rect.Right, rect.Top), new PointF(rect.Left, rect.Bottom) ), - _ => throw new ArgumentException($"{mode} mode is not supported.", nameof(mode)) - }; - return GetGradientVector(begPoint, endPoint); - } + LinearGradientMode.Horizontal => 0, + LinearGradientMode.Vertical => 90, + LinearGradientMode.ForwardDiagonal => 45, + LinearGradientMode.BackwardDiagonal => 135, + _ => throw new ArgumentException($"{mode} mode is not supported.", nameof(mode)) + }; + + private static float[] GetUniformArray(int length) + => length < 2 + ? throw new ArgumentException("at least two items are required.", nameof(length)) + : Enumerable.Range(0, length).Select(i => 1f * i / (length - 1)).ToArray(); - private static GradientVector GetGradientVector(RectangleF rect, float angle, bool scale) + static private Matrix GetTransform(RectangleF rect, float angle, bool scale) { - double radians = angle * (Math.PI / 180); - float midWidth = rect.Width / 2f; - float midHeight = rect.Height / 2f; + float radians = angle % 360 * (float)(Math.PI / 180); - float centerX = rect.Left + midWidth; - float centerY = rect.Top + midHeight; + float cos = (float)Math.Cos(radians); + float sin = (float)Math.Sin(radians); - float lengthX = scale ? midWidth : 1f; - float lengthY = scale ? midHeight : 1f; + float absCos = Math.Abs(cos); + float absSin = Math.Abs(sin); - var begPoint = new PointF( - centerX - lengthX * (float)Math.Cos(radians), - centerY - lengthY * (float)Math.Sin(radians)); + float wRatio = (absCos * rect.Width + absSin * rect.Height) / rect.Width; + float hRatio = (absSin * rect.Width + absCos * rect.Height) / rect.Height; - var endPoint = new PointF( - centerX + lengthX * (float)Math.Cos(radians), - centerY + lengthY * (float)Math.Sin(radians)); + float transX = rect.X + rect.Width / 2; + float transY = rect.Y + rect.Height / 2; - return GetGradientVector(begPoint, endPoint); - } + var transform = new Matrix(); + transform.Translate(transX, transY); + transform.Rotate(angle); + transform.Scale(wRatio, hRatio); + transform.Translate(-transX, -transY); - private static Color ApplyGamma(Color color, float gamma) - => Color.FromArgb( - color.A, - (int)(Math.Pow(color.R / 255.0, gamma) * 255), - (int)(Math.Pow(color.G / 255.0, gamma) * 255), - (int)(Math.Pow(color.B / 255.0, gamma) * 255)); + if (scale && absCos > 5e-4 && absSin > 5e-4) + { + var points = new PointF[3] + { + new(rect.Left, rect.Top), + new(rect.Right, rect.Top), + new(rect.Left, rect.Bottom), + }; - private static float[] CreateUniformArray(int length) - => length < 2 - ? throw new ArgumentException("at least two items are required.", nameof(length)) - : Enumerable.Range(0, length).Select(i => 1f * i / (length - 1)).ToArray(); + transform.TransformPoints(points); + + float ratio = rect.Width /rect.Height; + if (sin > 0 && cos > 0) + { + float slope = GetSlope(radians, ratio); + points[0].Y = (slope * (points[0].X - rect.Left)) + rect.Top; + points[1].X = ((points[1].Y - rect.Bottom) / slope) + rect.Right; + points[2].X = ((points[2].Y - rect.Top) / slope) + rect.Left; + } + else if (sin > 0 && cos < 0) + { + float slope = GetSlope(radians - 1 / 2f * Math.PI, ratio); + points[0].X = ((points[0].Y - rect.Bottom) / slope) + rect.Right; + points[1].Y = (slope * (points[1].X - rect.Right)) + rect.Bottom; + points[2].Y = (slope * (points[2].X - rect.Left)) + rect.Top; + } + else if (sin < 0 && cos < 0) + { + float slope = GetSlope(radians, ratio); + points[0].Y = (slope * (points[0].X - rect.Right)) + rect.Bottom; + points[1].X = ((points[1].Y - rect.Top) / slope) + rect.Left; + points[2].X = ((points[2].Y - rect.Bottom) / slope) + rect.Right; + } + else + { + float slope = GetSlope(radians - 3 / 2f * Math.PI, ratio); + points[0].X = ((points[0].Y - rect.Y) / slope) + rect.X; + points[1].Y = (slope * (points[1].X - rect.Left)) + rect.Top; + points[2].Y = (slope * (points[2].X - rect.Right)) + rect.Bottom; + } + + float m11 = (points[1].X - points[0].X) / rect.Width; + float m12 = (points[1].Y - points[0].Y) / rect.Width; + float m21 = (points[2].X - points[0].X) / rect.Height; + float m22 = (points[2].Y - points[0].Y) / rect.Height; + + transform = new Matrix(m11, m12, m21, m22, 0, 0); + transform.Translate(-rect.X, -rect.Y); + } + + return transform; + + static float GetSlope(double angleRadians, float aspectRatio) + => -1.0f / (aspectRatio * (float)Math.Tan(angleRadians)); + } void SigmaBellBlend(float focus, float scale, float sigma, float mean, float delta, int startIndex, int endIndex, bool invert) { @@ -416,30 +456,36 @@ static float Erf(float x, float sigma, float mean, int terms = 6) } } + private static Color ApplyGamma(Color color, float gamma) + => Color.FromArgb( + color.A, + (int)(Math.Pow(color.R / 255.0, gamma) * 255), + (int)(Math.Pow(color.G / 255.0, gamma) * 255), + (int)(Math.Pow(color.B / 255.0, gamma) * 255)); + private void UpdateShader(Action action) { action(); + var transform = new Matrix(m_transform.MatrixElements); switch (m_mode) { case WrapMode.TileFlipX: - m_transform.Scale(-1, 1); + transform.Scale(-1, 1); break; case WrapMode.TileFlipY: - m_transform.Scale(1, -1); + transform.Scale(1, -1); break; case WrapMode.TileFlipXY: - m_transform.Scale(-1, -1); + transform.Scale(-1, -1); break; } - var vector = GetGradientVector(m_rect, LinearGradientMode.ForwardDiagonal); + var start = new SKPoint(0, m_rect.Left); + var end = new SKPoint(0, m_rect.Right); + var matrix = transform.m_matrix; var gamma = m_gamma ? 2.2f : 1.0f; - - var start = vector.BegPoint.m_point; - var end = vector.EndPoint.m_point; var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; - var matrix = m_transform.m_matrix; int index; @@ -457,27 +503,25 @@ private void UpdateShader(Action action) blend[pos] = col; // specific color } - var lastColor = Color.Empty; - var blendKeys = blend.Keys.OrderBy(key => key); - - var positions = blendKeys.ToArray(); + var positions = blend.Keys.OrderBy(key => key).ToArray(); var colors = new SKColor[positions.Length]; - - index = 0; - foreach (var key in blendKeys) + + var lastColor = Color.Empty; + for (index = 0; index < positions.Length; index++) { + var key = positions[index]; var value = blend[key]; if (value is Color currColor) { var color = ApplyGamma(currColor, gamma); - colors[index++] = color.m_color; + colors[index] = color.m_color; lastColor = currColor; continue; } if (value is float factor) { var color = ApplyFactor(lastColor, factor); - colors[index++] = color.m_color; + colors[index] = color.m_color; continue; } } From 58786fe5aa1a5f6b0aba974e6ea40d7191d1c103 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 29 Jul 2024 01:27:21 -0300 Subject: [PATCH 073/217] add LinearGradientBrush unit test --- .gitignore | 1 + .../Drawing2D/LinearGradientBrushUnitTest.cs | 241 ++++++++++++++++++ test/Common/Utils.cs | 87 +++++++ .../res/images/brush/linear/angle-0.png | Bin 0 -> 246 bytes .../res/images/brush/linear/angle-45.png | Bin 0 -> 430 bytes .../res/images/brush/linear/angle-75.png | Bin 0 -> 2319 bytes .../res/images/brush/linear/angle-90.png | Bin 0 -> 271 bytes .../res/images/brush/linear/mode-bd.png | Bin 0 -> 499 bytes .../res/images/brush/linear/mode-fd.png | Bin 0 -> 430 bytes .../Common/res/images/brush/linear/mode-h.png | Bin 0 -> 246 bytes .../Common/res/images/brush/linear/mode-v.png | Bin 0 -> 271 bytes 11 files changed, 329 insertions(+) create mode 100644 test/Common/Drawing2D/LinearGradientBrushUnitTest.cs create mode 100644 test/Common/Utils.cs create mode 100644 test/Common/res/images/brush/linear/angle-0.png create mode 100644 test/Common/res/images/brush/linear/angle-45.png create mode 100644 test/Common/res/images/brush/linear/angle-75.png create mode 100644 test/Common/res/images/brush/linear/angle-90.png create mode 100644 test/Common/res/images/brush/linear/mode-bd.png create mode 100644 test/Common/res/images/brush/linear/mode-fd.png create mode 100644 test/Common/res/images/brush/linear/mode-h.png create mode 100644 test/Common/res/images/brush/linear/mode-v.png diff --git a/.gitignore b/.gitignore index cd0f87e..764e5a2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bin/ obj/ /_packages GeneXus.Drawing-DotNet.sln.DotSettings.user +/Test/Common/res/images/.out diff --git a/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs new file mode 100644 index 0000000..552b81c --- /dev/null +++ b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs @@ -0,0 +1,241 @@ +using System; +using System.IO; +using GeneXus.Drawing.Drawing2D; + +namespace GeneXus.Drawing.Test.Drawing2D; + +internal class LinearGradientBrushUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_Points() + { + var startPoint = new Point(0, 0); + var endPoint = new Point(1, 1); + + var bounds = new RectangleF(startPoint.X, startPoint.Y, startPoint.X + endPoint.X, startPoint.Y + endPoint.Y); + + var startColor = Color.Red; + var endColor = Color.Blue; + + using var brush = new LinearGradientBrush(startPoint, endPoint, startColor, endColor); + Assert.Multiple(() => + { + Assert.That(brush.LinearColors, Is.EqualTo(new[] { startColor, endColor })); + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile)); + }); + } + + [Test] + [TestCase(LinearGradientMode.Horizontal, "mode-h.png", new[] { 0.9999999f, 0f, 0f, 0.9999999f, 0f, 0f })] + [TestCase(LinearGradientMode.Vertical, "mode-v.png", new[] { -1.907349E-07f, 0.9999999f, -1f, -4.768372E-08f, 50f, 7.152557E-07f })] + [TestCase(LinearGradientMode.BackwardDiagonal, "mode-bd.png", new[] { -1f, 0.9999998f, -1f, -1f, 74.99999f, 25f })] + [TestCase(LinearGradientMode.ForwardDiagonal, "mode-fd.png", new[] { 0.9999999f, 0.9999999f, -1f, 0.9999999f, 25f, -25f })] + public void Constructor_RectangleMode(LinearGradientMode mode, string expected, float[] elements) + { + var rect = new RectangleF(15, 15, 20, 20); + + var startColor = Color.Red; + var endColor = Color.Blue; + + using var brush = new LinearGradientBrush(rect, startColor, endColor, mode); + Assert.Multiple(() => + { + Assert.That(brush.LinearColors, Is.EqualTo(new[] { startColor, endColor })); + Assert.That(brush.Transform.Elements, Is.EqualTo(elements).Within(1e-05)); + Assert.That(brush.Rectangle, Is.EqualTo(rect)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile)); + + string path = Path.Combine("brush", "linear", expected); + float similarity = Utils.CompareImage(path, brush, true); + Assert.That(similarity, Is.GreaterThan(0.95)); + }); + } + + [Test] + [TestCase(0f, "angle-0.png", new[] { 0.9999999f, 0f, 0f, 0.9999999f, 0f, 0f })] + [TestCase(45f, "angle-45.png", new[] { 0.999999f, 0.9999999f, -1f, 0.9999999f, 25f, -25f })] + [TestCase(75f, "angle-75.png", new[] { 0.3169872f, 1.183012f, -1.183013f, 0.3169872f, 46.65063f, -12.5f })] + [TestCase(90f, "angle-90.png", new[] { -1.907349E-07f, 0.9999999f, -1f, -4.768372E-08f, 50f, 7.152557E-07f })] + public void Constructor_RectangleAngle(float angle, string expected, float[] elements) + { + var rect = new RectangleF(15, 15, 20, 20); + + var startColor = Color.Red; + var endColor = Color.Blue; + + using var brush = new LinearGradientBrush(rect, startColor, endColor, angle); + Assert.Multiple(() => + { + Assert.That(brush.LinearColors, Is.EqualTo(new[] { startColor, endColor })); + Assert.That(brush.Transform.Elements, Is.EqualTo(elements).Within(1e-05)); + Assert.That(brush.Rectangle, Is.EqualTo(rect)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile)); + + string path = Path.Combine("brush", "linear", expected); + float similarity = Utils.CompareImage(path, brush, true); + Assert.That(similarity, Is.GreaterThan(0.95)); + }); + } + + [Test] + public void Method_Clone() + { + using var brush1 = new LinearGradientBrush(new Point(0, 0), new Point(1, 1), Color.Red, Color.Blue); + using var brush2 = brush1.Clone() as LinearGradientBrush; + Assert.Multiple(() => + { + Assert.That(brush2, Is.Not.Null); + Assert.That(brush2, Is.Not.SameAs(brush1)); + Assert.That(brush2.LinearColors, Is.EqualTo(brush1.LinearColors)); + Assert.That(brush2.Rectangle, Is.EqualTo(brush1.Rectangle)); + Assert.That(brush2.WrapMode, Is.EqualTo(brush1.WrapMode)); + }); + } + + [Test] + public void Property_Blend() + { + var bounds = new RectangleF(0, 0, 1, 1); + var colors = new Color[] { Color.Red, Color.Blue }; + + var blend = new Blend(4); + Array.Copy(new[] { 0f, 0.25f, 0.75f, 1f }, blend.Positions, 4); + Array.Copy(new[] { 0f, 0.25f, 0.25f, 0f }, blend.Factors, 4); + + using var brush = new LinearGradientBrush(bounds, colors[0], colors[1], 0) { Blend = blend }; + Assert.Multiple(() => + { + Assert.That(brush.Blend.Factors, Is.EqualTo(blend.Factors)); + Assert.That(brush.Blend.Positions, Is.EqualTo(blend.Positions)); + Assert.That(brush.LinearColors, Is.EqualTo(colors)); + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile)); + }); + } + + [Test] + public void Property_Interpolation_And_Gamma() + { + var bounds = new RectangleF(0, 0, 1, 1); + var colors = new Color[] { Color.Red, Color.Blue }; + + var interpolation = new ColorBlend(3); + Array.Copy(new[] { 0f, 0.5f, 1f }, interpolation.Positions, 3); + Array.Copy(new[] { Color.Cyan, Color.Yellow, Color.Magenta }, interpolation.Colors, 3); + + var gammaCorrection = true; + + using var brush = new LinearGradientBrush(bounds, colors[0], colors[1], 0) { InterpolationColors = interpolation, GammaCorrection = gammaCorrection }; + Assert.Multiple(() => + { + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(interpolation.Colors)); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(interpolation.Positions)); + Assert.That(brush.GammaCorrection, Is.EqualTo(gammaCorrection)); + Assert.That(brush.LinearColors, Is.EqualTo(interpolation.Colors)); // NOTE: interpolation overrides linear colors + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile)); + }); + } + + [Test] + public void Property_Transform() + { + var bounds = new RectangleF(0, 0, 1, 1); + var colors = new Color[] { Color.Red, Color.Blue }; + var matrix = new Matrix(1, 2, 3, 4, 5, 6); + + using var brush = new LinearGradientBrush(bounds, colors[0], colors[1], 0) { Transform = matrix }; + Assert.Multiple(() => + { + Assert.That(brush.LinearColors, Is.EqualTo(colors)); + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.Transform, Is.EqualTo(matrix)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile)); + }); + } + + [Test] + public void Property_WrapMode() + { + var bounds = new RectangleF(0, 0, 1, 1); + var colors = new Color[] { Color.Red, Color.Blue }; + var wrapMode = WrapMode.Clamp; + + using var brush = new LinearGradientBrush(bounds, colors[0], colors[1], 0) { WrapMode = wrapMode }; + Assert.Multiple(() => + { + Assert.That(brush.LinearColors, Is.EqualTo(colors)); + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(wrapMode)); + }); + } + + [Test] + public void Method_SetSigmaBellShape() + { + var startPoint = new Point(0, 0); + var endPoint = new Point(1, 1); + + var startColor = Color.Red; + var endColor = Color.Blue; + + var (focus, scale) = (0.25f, 0.75f); + + using var brush = new LinearGradientBrush(startPoint, endPoint, startColor, endColor); + brush.SetSigmaBellShape(focus, scale); + Assert.Multiple(() => + { + var threshold = 1e-3f; + var length = brush.Blend.Factors.Length; + var mid = length / 2; + + Assert.That(length, Is.EqualTo(511)); + + Assert.That(brush.Blend.Factors[0], Is.EqualTo(0)); + Assert.That(brush.Blend.Positions[0], Is.EqualTo(0)); + + Assert.That(brush.Blend.Factors[1], Is.EqualTo(0.000675).Within(threshold)); + Assert.That(brush.Blend.Positions[1], Is.EqualTo(0.000980).Within(threshold)); + + Assert.That(brush.Blend.Factors[mid], Is.EqualTo(scale)); + Assert.That(brush.Blend.Positions[mid], Is.EqualTo(focus)); + + Assert.That(brush.Blend.Factors[length - 2], Is.EqualTo(0.000675).Within(threshold)); + Assert.That(brush.Blend.Positions[length - 2], Is.EqualTo(0.997058).Within(threshold)); + + Assert.That(brush.Blend.Factors[length - 1], Is.EqualTo(0)); + Assert.That(brush.Blend.Positions[length - 1], Is.EqualTo(1)); + }); + } + + [Test] + public void Method_SetBlendTriangularShape() + { + var startPoint = new Point(0, 0); + var endPoint = new Point(1, 1); + + var startColor = Color.Red; + var endColor = Color.Blue; + + var (focus, scale) = (0.25f, 0.75f); + + using var brush = new LinearGradientBrush(startPoint, endPoint, startColor, endColor); + brush.SetBlendTriangularShape(focus, scale); + Assert.Multiple(() => + { + Assert.That(brush.Blend.Factors.Length, Is.EqualTo(3)); + Assert.That(brush.Blend.Positions[0], Is.EqualTo(0)); + Assert.That(brush.Blend.Factors[0], Is.EqualTo(0)); + Assert.That(brush.Blend.Positions[1], Is.EqualTo(focus)); + Assert.That(brush.Blend.Factors[1], Is.EqualTo(scale)); + Assert.That(brush.Blend.Positions[2], Is.EqualTo(1)); + Assert.That(brush.Blend.Factors[2], Is.EqualTo(0)); + }); + } +} \ No newline at end of file diff --git a/test/Common/Utils.cs b/test/Common/Utils.cs new file mode 100644 index 0000000..0dee0f7 --- /dev/null +++ b/test/Common/Utils.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; + +namespace GeneXus.Drawing.Test.Drawing2D; + +internal abstract class Utils +{ + private static readonly string IMAGE_PATH = Path.Combine( + Directory.GetParent(Environment.CurrentDirectory).Parent.FullName, + "res", "images"); + + public static double DeltaE(Color color1, Color color2) + { + var lab1 = RgbToLab(color1); + var lab2 = RgbToLab(color2); + + // human eye cannot distinguish colors below 1 DeltaE + return DistanceCie76(lab1, lab2, color1.A / 255.0, color2.A / 255.0); + } + + private static double[] RgbToLab(Color color) + { + // convert RGB to XYZ + double r = PivotRgb(color.R / 255.0); + double g = PivotRgb(color.G / 255.0); + double b = PivotRgb(color.B / 255.0); + + double x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375; + double y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750; + double z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041; + + // convert XYZ to LAB + double[] xyz = { x / 0.95047, y / 1.00000, z / 1.08883 }; + for (int i = 0; i < 3; i++) + xyz[i] = PivotXyz(xyz[i]); + + return new[] + { + 116.0 * xyz[1] - 16, + 500.0 * (xyz[0] - xyz[1]), + 200.0 * (xyz[1] - xyz[2]) + }; + } + + private static double PivotRgb(double n) + => (n > 0.04045) ? Math.Pow((n + 0.055) / 1.055, 2.4) : n / 12.92; + + private static double PivotXyz(double n) + => (n > 0.008856) ? Math.Pow(n, 1.0 / 3.0) : (7.787 * n) + (16.0 / 116.0); + + private static double DistanceCie76(double[] lab1, double[] lab2, double alpha1, double alpha2) + => Math.Sqrt( + Math.Pow(lab2[0] - lab1[0], 2) + + Math.Pow(lab2[1] - lab1[1], 2) + + Math.Pow(lab2[2] - lab1[2], 2) + + Math.Pow(alpha1 - alpha2, 2) + ); + + public static float CompareImage(string filename, Brush brush, bool save = false) + { + string filepath = Path.Combine(IMAGE_PATH, filename); + using var im = Image.FromFile(filepath); + using var bm = new Bitmap(im); + + var gu = GraphicsUnit.Pixel; + using var bg = new Bitmap(bm.Width, bm.Height); + using var g = Graphics.FromImage(bg); + g.FillRectangle(brush, bg.GetBounds(ref gu)); + + float hits = 0f; // compare pixel to pixel + for (int i = 0; i < bg.Width; i++) + for (int j = 0; j < bg.Height; j++) + hits += DeltaE(bg.GetPixel(i, j), bm.GetPixel(i, j)) < 10 ? 1 : 0; + + if (save) + { + string savepath = Path.Combine(IMAGE_PATH, ".out", filename); + + var dirpath = Path.GetDirectoryName(savepath); + if (!Directory.Exists(dirpath)) Directory.CreateDirectory(dirpath); + + bg.Save(savepath); + } + + return hits / (bg.Width * bg.Height); + } +} \ No newline at end of file diff --git a/test/Common/res/images/brush/linear/angle-0.png b/test/Common/res/images/brush/linear/angle-0.png new file mode 100644 index 0000000000000000000000000000000000000000..bd74fcf60d88dc89c0c5069f96ff7db9e459db72 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V7I4>V@QVc+w%tn8Uh$xFKSP4VBl1l;K0NP zBp5hZCL9p|!33lnnDpDia@QT0X&KTca!khO=FH6-Q&P_!nl{r`*xk*GEvxEWNlfO> z%O6xGw5yrt>g}5I<>zVRq$3@I%5FUph6EXQ6{`9lzm)0x-?Tp%=qv_LS3j3^P6!lvI6;>1s;*b3=DjSL74G){)!Z!24_zf$B+!?w>KwlV`5}*z33b{#WheYHE;?q z5W7Z-06DIaCO`}pzF7PI8|%Y0yPxr&dvH7u9f z+wPfsU9?m^*Y?E=zi{)aU6-!6HrBmWW_)NTB=C+h>i#~CBlYr}9N#B46p%~B|Gl}o gr2q%1q4AY{tyo!V;Uvj2U>Gxay85}Sb4q9e0446G-~a#s literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/linear/angle-75.png b/test/Common/res/images/brush/linear/angle-75.png new file mode 100644 index 0000000000000000000000000000000000000000..39ab9497baa6eaebbd7cbd3415c65f8bd374a140 GIT binary patch literal 2319 zcmZWreK?bQAHPTGjFv)qG;Jkuj@NJ4n*sJ~?E*7b-eFz} z40OurD0C@^c3K~8nqyzIr^V5t#f@!U>7>^UG9#O}MBCORUdGB!Sv2?^>8udBf+V%J$^D$Xk2* zn51W$_%E3xF_L1;tCjymtA|hJ${iT?ytWNO+f>kEuq!u6bAgtjQfxvF!7XX4Q1$Yd zi1n{!fn`~xuJiV|)!1#6!T(?s8%whg#qSS|<)Ave!bU@0SX^(TAHysoa z64`L{vkwkJWcx=Pj@;@QX(4U5LZ&&C$;L{a9N5v7Ri5Ea&aozIYD&5))py~fj-MTb zupwCl?OL|%80yl@;T2nHfY(N5m)q%jdFL-ynD2?d9>HvWE~XSv#Q2sC#ZQFol(FoH zD}?Q>_?99<>ZP5;_9GbXGz(#kd9N-(gktXRd`&5h61a#7u0+0VEZ?Oi5_PUG44p8& zR~Xf5-uC793;!NOqZ7Ru-DlM{GcD_V`BhZ8MJk5Kp^8QBQ>VD>Zg*?UI8nglY_(1t za*{J(NF>Ihnm8U=is0oH{hnhUM@wdOKAv8FPewS@{_CMOtTTHGF1M3p$5S!HUV&nS zGZXBF=2EMPzLKI`w}etk;|>*3V9OkZIQ2H4M1~{EYn@(RRVNP`lWL6`74e#a23gDp zCh4%rwH!WG4;7|*Y#|XV4ICr}*rcbPBo2hxM&D+MA$}G)LSU0QJhJA4=E|trxGORf zF$`;ZV3nr$tjQFqt#$1wOt&d?B5FgfM1ahNDrlp>W>T^1wtTHE`~`&TWP`E55K}}U zgEq)GS}ZvNPOg_P(>PuPQwx>r3gr%y0Rj9ZxO*sOL>Sbvh-0XVS3qgRKvhk$g0i~S zU1^(g&iB4~E&ckx9#t^m+n8dR^QR`C@cwY@JH9K>vQ1qX8=R=y?!Hoo?Bm|smx-1E)qZhyb=tG4 zPCTcN;FcP-uLbgm_s(ECzl~e`Pyz|W$h=5QXle3Wn|AJToJQQwMpxWIq@!0P*^DQB zH$EL?8?5Y0AEcjN@E2E54HT>s!#BwpREfYK*2KQ2lsl0EM%G!f#=pP(-rlaPrtHkw zRjWpiR!RF3mswY?A+;88*p)eaP(3%pg<35fBOlDe!$UEf^wfB7-p*T2o@%|&W!hUK zpBh=+nAfT?Sx?DRJRSdkg$*jB$}1#Agx=XC+vwJ!E(;f> zon<2s z^F30F%U9L9^<;VflD6}fV&%B_L-grC&<#~rC}l*QfpbE@lAI*@;tX{(6)-2D5bN?h zl-fLHRH!ab#<_j}|0$2cRR}6HEgpU8o8fF7GP!#J8OYZnpNgxs$V~#suXyJ@UmKAT z)?gZ@C)`bWx)`g>65E=biRl56!$fGkQDBHHQ!1qqk7!Gx?KKVPYXAf zU@zClbb2LBkiL1gzbJjPl6(99-zwCm8Skboe3864Tu6bAsV16En`AtnOO=2VBmoPY zU20|hzN_H7x$wP;1r?t;ETShKr1Pge!XUkdU?J_R);r^8uTKr`VzHkeqT zeg{DrcM^Tjj)XZ?TQ42MGDFrrjfKdW;hQ4tB>d!K@Ge) zwJgP`<1+%7g%Zun0fuVYGGhwYfCbvP*?mn@xHKa**ZM627@KISIuM%X?4gQv%z!wq zCh73pKR-GG!}R%OdrPoDU9j8V`CqVrn<{p~d!hFxbO)M2;wk60kM|bo&sRYC!+`9i zuE{gG=o9BQ7to6<1nWMby^;g<7E9+4z&Nf^o9mH08UifR)asxz>a<+2PS1~F)B){@ z)nh5|5V+9%|D`*={LkLm{Zjh%A>g-%+J#5e`XW(Yu;|%EwZ#zP(j{nCJdeznnr}K| z+e+6%L5AlQ;)}|Y0P*LGu1=8n&COdL<0CGO1#qxgi5$God2*d~bRr?-4|6c(@ Bit+#e literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/linear/angle-90.png b/test/Common/res/images/brush/linear/angle-90.png new file mode 100644 index 0000000000000000000000000000000000000000..9551a46fab32ffc1c6508e2f3d60626767ebd078 GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;1W+4$B+!?x7ROiwIW{f!cOY?cpFrZDrVStg`UV>T1BOh}!|EM{hzkT#uJjn6XS z$rNTbJG;?NznmceRj)gQ=*aX^12!lvI6;>1s;*b3=DjSL74G){)!Z!h7L~`$B+!?yEkR0I0cHdJ)FKtqkECgsYM#y zn>0=>(&+*+khs5QHAZG%Tl4efy}OogjsG32y|z<7_gIf<;u1~gvl1D8@!VBWm#%j8 zUE8vC;Zj}Sw;37!vFuq^L03Dw&P^#@xJ29au7<|~CUeo9tB$y)o?BHAG<9*7Oi22A zHfz(HtB$(mUVF76X!_DBoh#|<7>#*nW*u?qual2FHqS6+YW2|x&n0b>PW<+-I-J0H zMwrbzVV*$*)903hLNnBPrX8?J+`#gTvsu-^UqYv`@(=+f_qOe1{@n7g_K&?FGe6G5@sC3cY>fOE4=+xBFJaUxn|wm2yi4)AvDJ|Y zzvq{<9Tb_NjA^sY-*f-8d?r}`);2i!?#)I+$M>^mlHymp8s9`?9kvt?G$fCNQEHJYD@<);T3K0RTk+(_sJr literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/linear/mode-fd.png b/test/Common/res/images/brush/linear/mode-fd.png new file mode 100644 index 0000000000000000000000000000000000000000..a4f0004c596a3a8cb31933d4d6240326e9b4fe58 GIT binary patch literal 430 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!24_zf$B+!?w>KwlV`5}*z33b{#WheYHE;?q z5W7Z-06DIaCO`}pzF7PI8|%Y0yPxr&dvH7u9f z+wPfsU9?m^*Y?E=zi{)aU6-!6HrBmWW_)NTB=C+h>i#~CBlYr}9N#B46p%~B|Gl}o gr2q%1q4AY{tyo!V;Uvj2U>Gxay85}Sb4q9e0446G-~a#s literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/linear/mode-h.png b/test/Common/res/images/brush/linear/mode-h.png new file mode 100644 index 0000000000000000000000000000000000000000..bd74fcf60d88dc89c0c5069f96ff7db9e459db72 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V7I4>V@QVc+w%tn8Uh$xFKSP4VBl1l;K0NP zBp5hZCL9p|!33lnnDpDia@QT0X&KTca!khO=FH6-Q&P_!nl{r`*xk*GEvxEWNlfO> z%O6xGw5yrt>g}5I<>zVRq$3@I%5FUph6EXQ6{`9lzm)0x-?Tp%=qv_LS3j3^P6!lvI6;>1s;*b3=DjSL74G){)!Z!;1W+4$B+!?x7ROiwIW{f!cOY?cpFrZDrVStg`UV>T1BOh}!|EM{hzkT#uJjn6XS z$rNTbJG;?NznmceRj)gQ=*aX^12 Date: Mon, 29 Jul 2024 01:37:44 -0300 Subject: [PATCH 074/217] minor fix in namespace for Utils class --- test/Common/Utils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Common/Utils.cs b/test/Common/Utils.cs index 0dee0f7..e36ad3e 100644 --- a/test/Common/Utils.cs +++ b/test/Common/Utils.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace GeneXus.Drawing.Test.Drawing2D; +namespace GeneXus.Drawing.Test; internal abstract class Utils { From 710866e5a98519ebb39bdda99dbc59101974d462 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 29 Jul 2024 01:38:59 -0300 Subject: [PATCH 075/217] improve SolidBrush unit test --- test/Common/SolidBrushUnitTest.cs | 18 +++++++++++++++--- .../res/images/brush/solid/color-blue.png | Bin 0 -> 228 bytes .../res/images/brush/solid/color-green.png | Bin 0 -> 228 bytes .../res/images/brush/solid/color-red.png | Bin 0 -> 187 bytes 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 test/Common/res/images/brush/solid/color-blue.png create mode 100644 test/Common/res/images/brush/solid/color-green.png create mode 100644 test/Common/res/images/brush/solid/color-red.png diff --git a/test/Common/SolidBrushUnitTest.cs b/test/Common/SolidBrushUnitTest.cs index 6c2e889..8f52ffd 100644 --- a/test/Common/SolidBrushUnitTest.cs +++ b/test/Common/SolidBrushUnitTest.cs @@ -1,3 +1,5 @@ +using System.IO; + namespace GeneXus.Drawing.Test; internal class SolidBrushUnitTest @@ -8,12 +10,22 @@ public void Setup() } [Test] - public void Constructor_Color() + [TestCase("#FF0000", "color-red.png")] + [TestCase("#00FF00", "color-green.png")] + [TestCase("#0000FF", "color-blue.png")] + public void Constructor_Color(string hex, string expected) { - var color = Color.Red; + var color = new Color(hex); using var brush = new SolidBrush(color); - Assert.That(brush.Color, Is.EqualTo(color)); + Assert.Multiple(() => + { + Assert.That(brush.Color, Is.EqualTo(color)); + + string path = Path.Combine("brush", "solid", expected); + float similarity = Utils.CompareImage(path, brush, true); + Assert.That(similarity, Is.GreaterThan(0.95)); + }); } [Test] diff --git a/test/Common/res/images/brush/solid/color-blue.png b/test/Common/res/images/brush/solid/color-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..ae82c5d36fdf7c365b36e28cdff1dec8020fc68a GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V5O&vV@QVc-4lk43!lvI6;>1s;*b3=DjSL74G){)!Z!V5O&vV@QVc-4lk43!lvI6;>1s;*b3=DjSL74G){)!Z!V1TEKV@QVc+w+dR3B8;nb*{b2hh1T>q$)78&qol`;+0Gwem-~a#s literal 0 HcmV?d00001 From fdebe36d4790cf74fc212b5bd4a51a598641a17e Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 29 Jul 2024 22:58:24 -0300 Subject: [PATCH 076/217] add Bitmap.Clone overload with RectangleF --- src/Common/Bitmap.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 1f8f996..188e765 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -123,13 +123,20 @@ public Bitmap(Image original, Size size) /// Creates a copy of the section of this defined /// by structure and with a specified PixelFormat enumeration. /// - public object Clone(Rectangle rect, PixelFormat format) + public object Clone(RectangleF rect, PixelFormat format) { var bitmap = new Bitmap(rect.Width, rect.Height); var portion = SKRectI.Truncate(rect.m_rect); return m_bitmap.ExtractSubset(bitmap.m_bitmap, portion) ? bitmap : Clone(); } + /// + /// Creates a copy of the section of this defined + /// by structure and with a specified PixelFormat enumeration. + /// + public object Clone(Rectangle rect, PixelFormat format) + => Clone(new RectangleF(rect.m_rect), format); + #endregion From 3c8d4dba10517621c9b10e1b79654496a433f7a8 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 29 Jul 2024 23:47:45 -0300 Subject: [PATCH 077/217] fix TextureBrush constructors singnatures --- src/Common/TextureBrush.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Common/TextureBrush.cs b/src/Common/TextureBrush.cs index e2f82ef..1611c2e 100644 --- a/src/Common/TextureBrush.cs +++ b/src/Common/TextureBrush.cs @@ -22,31 +22,31 @@ private TextureBrush(RectangleF rect, Image image, WrapMode mode) /// /// Initializes a new object that uses the - /// specified image, wrap mode, and bounding rectangle. + /// specified image and wrap mode. /// - public TextureBrush(Image image, RectangleF rect, WrapMode mode = WrapMode.Tile) - : this(rect, image, mode) { } + public TextureBrush(Image image, WrapMode mode = WrapMode.Tile) + : this(image, mode, new Rectangle(0, 0, image.Size)) { } /// /// Initializes a new object that uses the /// specified image, wrap mode, and bounding rectangle. /// - public TextureBrush(Image image, Rectangle rect, WrapMode mode = WrapMode.Tile) - : this(new RectangleF(rect.m_rect), image, mode) { } + public TextureBrush(Image image, WrapMode mode, RectangleF rect) + : this(rect, image, mode) { } /// /// Initializes a new object that uses the - /// specified image and wrap mode. + /// specified image, wrap mode, and bounding rectangle. /// - public TextureBrush(Image image, WrapMode mode = WrapMode.Tile) - : this(image, new Rectangle(0, 0, image.Size), mode) { } + public TextureBrush(Image image, WrapMode mode, Rectangle rect) + : this(image, mode, new RectangleF(rect.m_rect)) { } /// /// Initializes a new object that uses /// the specified image, bounding rectangle, and image attributes. /// public TextureBrush(Image image, RectangleF rect, object imageAttributes) - : this(image, rect, WrapMode.Tile) { } // TODO: implement ImageAttributes class + : this(image, WrapMode.Tile, rect) { } // TODO: implement ImageAttributes class /// /// Initializes a new object that uses From 054a33084e9fc0fb69f304af577d57e281b8ef33 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 29 Jul 2024 23:48:42 -0300 Subject: [PATCH 078/217] fix UpdateShader method from TextureBrush, including matrix in the code --- src/Common/TextureBrush.cs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Common/TextureBrush.cs b/src/Common/TextureBrush.cs index 1611c2e..115ebd0 100644 --- a/src/Common/TextureBrush.cs +++ b/src/Common/TextureBrush.cs @@ -155,25 +155,25 @@ private void UpdateShader(Action action) }; var info = new SKImageInfo((int)m_bounds.Width, (int)m_bounds.Height); + var matrix = Transform.m_matrix; - using var surfece = SKSurface.Create(info); - switch (m_image) + using var pixmap = m_image switch { - case Bitmap bitmap: - surfece.Canvas.DrawBitmap(bitmap.m_bitmap, m_bounds.m_rect); - break; - - case Svg svg: - surfece.Canvas.DrawImage(svg.InnerImage, m_bounds.m_rect); - break; - - default: - throw new NotImplementedException($"image type {m_image.GetType().Name}."); - } + Bitmap bm => bm.m_bitmap.PeekPixels(), + Svg svg => svg.InnerImage.PeekPixels(), + _ => throw new NotImplementedException($"image type {m_image.GetType().Name}.") + }; + + using var bitmap = new SKBitmap(); + using var subset = pixmap.ExtractSubset(SKRectI.Round(m_bounds.m_rect)); + bitmap.InstallPixels(subset); + + using var surfece = SKSurface.Create(info); + surfece.Canvas.DrawBitmap(bitmap, 0, 0); - var src = surfece.Snapshot(); + using var src = surfece.Snapshot(); - m_paint.Shader = SKShader.CreateImage(src, tmx, tmy); + m_paint.Shader = SKShader.CreateImage(src, tmx, tmy, matrix); } #endregion From 9781a50be368b1600544396f9970eeb0324c8c0e Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 29 Jul 2024 23:50:03 -0300 Subject: [PATCH 079/217] fix TextureBrushUnitTest --- test/Common/TextureBrushUnitTest.cs | 22 ++++++++++++------ .../res/images/brush/textured/wrap-clamp.png | Bin 0 -> 712 bytes .../res/images/brush/textured/wrap-tile.png | Bin 0 -> 788 bytes 3 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 test/Common/res/images/brush/textured/wrap-clamp.png create mode 100644 test/Common/res/images/brush/textured/wrap-tile.png diff --git a/test/Common/TextureBrushUnitTest.cs b/test/Common/TextureBrushUnitTest.cs index 6e9eb8d..7204ddc 100644 --- a/test/Common/TextureBrushUnitTest.cs +++ b/test/Common/TextureBrushUnitTest.cs @@ -16,20 +16,28 @@ public void Setup() } [Test] - public void Constructor_BitmapRectWrap() + [TestCase(WrapMode.Tile, "wrap-tile.png")] + [TestCase(WrapMode.Clamp, "wrap-clamp.png")] + public void Constructor_BitmapRectWrap(WrapMode mode, string expected) { var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); using var bitmap = new Bitmap(filePath); - var rect = new Rectangle(0, 0, 100, 50); - var wrapMode = WrapMode.Tile; - var matrix = new Matrix(1, 2, 3, 4, 5, 6); + var rect = new RectangleF(15, 15, 20, 20); - using var brush = new TextureBrush(bitmap, rect, wrapMode) { Transform = matrix }; + var matrix = new Matrix(); + matrix.Rotate(45); + matrix.Translate(10, 10); + + using var brush = new TextureBrush(bitmap, mode, rect) { Transform = matrix }; Assert.Multiple(() => { Assert.That(brush.Image, Is.EqualTo(bitmap)); - Assert.That(brush.WrapMode, Is.EqualTo(wrapMode)); + Assert.That(brush.WrapMode, Is.EqualTo(mode)); Assert.That(brush.Transform, Is.EqualTo(matrix)); + + string path = Path.Combine("brush", "textured", expected); + float similarity = Utils.CompareImage(path, brush, true); + Assert.That(similarity, Is.GreaterThan(0.95)); }); } @@ -39,7 +47,7 @@ public void Method_Clone() var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); using var bitmap = new Bitmap(filePath); - using var brush1 = new TextureBrush(bitmap, new Rectangle(0, 0, 100, 50), WrapMode.Tile) { Transform = new Matrix(1, 2, 3, 4, 5, 6) }; + using var brush1 = new TextureBrush(bitmap, WrapMode.Tile, new Rectangle(0, 0, 100, 50)) { Transform = new Matrix(1, 2, 3, 4, 5, 6) }; using var brush2 = brush1.Clone() as TextureBrush; Assert.Multiple(() => { diff --git a/test/Common/res/images/brush/textured/wrap-clamp.png b/test/Common/res/images/brush/textured/wrap-clamp.png new file mode 100644 index 0000000000000000000000000000000000000000..994687f282725c2216fe7ac51e9744f4796f8455 GIT binary patch literal 712 zcmV;(0yq7MP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0$oW&K~!i%?Uv1J z6Hye!<3FKkg(9^p!G$YV1#v4b#hn{B1;M@k1KGK#Yr$O=RB+|OntsKmPE+X+OG~ZE zOiVIOYB4dS3N0MxJl~5oJZx*>Im-RPfj4h)IhS`oCbid^#MhlRKCNX?FWf|DV}-91WQw+XnAMx zW${d}XAY7M>z-=%G+sRzj*Sl}O~z;5@nx74ORrX%v6{9?hjkAd``P%2N`j@uGw*n2 z&u{>6n$?RLd|Z2iYT5s7ujsJuVL7p>{ij1pJoD}-iJZ8a=}CumkLeNa4vJ@rrNs$W z^==>vmwqwcuS6uml43G^36qFw_IPq}471F5U-PaZD(4cMNNm`9JklG#EU(SEVO3t- zCE&zly?hG!$5B@`C@}K!>q%6uQTl=`K^y2lux@jo@cUplONY-Wxl9o9WIGslq* uD+q!h2!bF8f*=TjAP9mW2*Q6xD)k*7{9Xsq@r#N80000WP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0;x$vK~!i%?U&7K z6G0fp$A3bT3Poy9f(K8Y3gT5fiZ?G_3W9h22XgaJ&w{rqsNl(iHT{ZBO;hO-OG~ZE zCg!6_EhdIkp@qkNMs^+3-6lcEt7Q&9cr&|s9%kQ}cji@x%^7^(Z{f>k0@dto)XVGG z&&E*PdW}w}gWcK|R+10UZnyRCUP$SwgT0riI`>dpn$VQ8cW|(F8pV%Ga6e3=xNrr# zJKIMLqL9*4mGm|2rDA9;kNo0Y9Pa#O6qYCP(g40U$CFAD5aHFH;=}=2XW1e7juj==bxC`N`5}nBn&A%#f|e~YFxj>)G_85W3EYV zFjk9dE`iUR&rmKn$F6NDJ;ii!(>TvZHHf9K>( z#P;kF*q%Lte)Vk69)*;i?b*XjiIkr0*-cDiDLvb>M;Z(1*`D3RG?vn{J$n>VdbVfp z#jy2k&;GArd-f=7&mMv8*%|W+z)P9!*#-1$&mLw79_CJ%*oiH_Hs@ Sc446a0000 Date: Tue, 30 Jul 2024 12:55:11 -0300 Subject: [PATCH 080/217] minor change in summary for HatchBrush constructor --- src/Common/Drawing2D/HatchBrush.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/HatchBrush.cs b/src/Common/Drawing2D/HatchBrush.cs index abd3338..19ae934 100644 --- a/src/Common/Drawing2D/HatchBrush.cs +++ b/src/Common/Drawing2D/HatchBrush.cs @@ -11,7 +11,7 @@ public sealed class HatchBrush : Brush /// /// Initializes a new instance of the class with the - /// specified enumeration, foreground color, and background color. + /// specified enumeration, foreground color, and background color. /// public HatchBrush(HatchStyle hatchStyle, Color foreColor, Color backColor) : base(new SKPaint { }) @@ -25,7 +25,7 @@ public HatchBrush(HatchStyle hatchStyle, Color foreColor, Color backColor) /// /// Initializes a new instance of the class with the - /// specified enumeration and foreground color. + /// specified enumeration and foreground color. /// public HatchBrush(HatchStyle hatchStyle, Color foreColor) : this(hatchStyle, foreColor, Color.Transparent) { } From 1ab0f613fc9563d9966d125d730ec9d0e9bf7e0c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Sun, 4 Aug 2024 13:45:09 -0300 Subject: [PATCH 081/217] simplify HatchBrush.UpdateShader method and improve readability --- src/Common/Drawing2D/HatchBrush.cs | 349 +++++++---------------------- 1 file changed, 75 insertions(+), 274 deletions(-) diff --git a/src/Common/Drawing2D/HatchBrush.cs b/src/Common/Drawing2D/HatchBrush.cs index 19ae934..3260b93 100644 --- a/src/Common/Drawing2D/HatchBrush.cs +++ b/src/Common/Drawing2D/HatchBrush.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using SkiaSharp; namespace GeneXus.Drawing.Drawing2D; @@ -63,293 +64,93 @@ public override object Clone() #region Utilities + private static readonly Dictionary HATCH_DATA = new() + { + { HatchStyle.Horizontal, (8, new uint[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }) }, + { HatchStyle.Vertical, (8, new uint[] { 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }) }, + { HatchStyle.ForwardDiagonal, (8, new uint[] { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }) }, + { HatchStyle.BackwardDiagonal, (8, new uint[] { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 }) }, + { HatchStyle.Cross, (8, new uint[] { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }) }, + { HatchStyle.DiagonalCross, (8, new uint[] { 0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81 }) }, + { HatchStyle.Percent05, (8, new uint[] { 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 }) }, + { HatchStyle.Percent10, (8, new uint[] { 0x80, 0x00, 0x08, 0x00 }) }, + { HatchStyle.Percent20, (4, new uint[] { 0x08, 0x00, 0x02, 0x00 }) }, + { HatchStyle.Percent25, (4, new uint[] { 0x08, 0x02 }) }, + { HatchStyle.Percent30, (4, new uint[] { 0x0A, 0x04, 0x0A, 0x01 }) }, + { HatchStyle.Percent40, (8, new uint[] { 0xAA, 0x55, 0xAA, 0x51, 0xAA, 0x55, 0xAA, 0x15 }) }, + { HatchStyle.Percent50, (2, new uint[] { 0x02, 0x01 }) }, + { HatchStyle.Percent60, (4, new uint[] { 0x0E, 0x05, 0x0B, 0x05 }) }, + { HatchStyle.Percent70, (4, new uint[] { 0x07, 0x0D }) }, + { HatchStyle.Percent75, (4, new uint[] { 0x07, 0x0F, 0x0D, 0x0F }) }, + { HatchStyle.Percent80, (8, new uint[] { 0xEF, 0xFF, 0x7E, 0xFF }) }, + { HatchStyle.Percent90, (8, new uint[] { 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0x7F }) }, + { HatchStyle.LightDownwardDiagonal, (4, new uint[] { 0x08, 0x04, 0x02, 0x01 }) }, + { HatchStyle.LightUpwardDiagonal, (4, new uint[] { 0x01, 0x02, 0x04, 0x08 }) }, + { HatchStyle.DarkDownwardDiagonal, (4, new uint[] { 0x0C, 0x06, 0x03, 0x09 }) }, + { HatchStyle.DarkUpwardDiagonal, (4, new uint[] { 0x03, 0x06, 0x0C, 0x09 }) }, + { HatchStyle.WideDownwardDiagonal, (8, new uint[] { 0xC1, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x83 }) }, + { HatchStyle.WideUpwardDiagonal, (8, new uint[] { 0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xC1 }) }, + { HatchStyle.LightVertical, (4, new uint[] { 0x08, 0x08, 0x08, 0x08 }) }, + { HatchStyle.LightHorizontal, (4, new uint[] { 0x0F, 0x00, 0x00, 0x00 }) }, + { HatchStyle.NarrowVertical, (2, new uint[] { 0x01, 0x01 }) }, + { HatchStyle.NarrowHorizontal, (2, new uint[] { 0x03, 0x00 }) }, + { HatchStyle.DarkVertical, (4, new uint[] { 0x0C, 0x0C, 0x0C, 0x0C }) }, + { HatchStyle.DarkHorizontal, (4, new uint[] { 0x0F, 0x0F, 0x00, 0x00 }) }, + { HatchStyle.DashedDownwardDiagonal, (4, new uint[] { 0x00, 0x00, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00 }) }, + { HatchStyle.DashedUpwardDiagonal, (4, new uint[] { 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x00, 0x00 }) }, + { HatchStyle.DashedHorizontal, (8, new uint[] { 0xF0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00 }) }, + { HatchStyle.DashedVertical, (8, new uint[] { 0x80, 0x80, 0x80, 0x80, 0x08, 0x08, 0x08, 0x08 }) }, + { HatchStyle.SmallConfetti, (8, new uint[] { 0x80, 0x08, 0x40, 0x03, 0x10, 0x01, 0x20, 0x04 }) }, + { HatchStyle.LargeConfetti, (8, new uint[] { 0xB1, 0x30, 0x03, 0x1B, 0xD8, 0xC0, 0x06, 0x8D }) }, + { HatchStyle.ZigZag, (8, new uint[] { 0x81, 0x42, 0x24, 0x18 }) }, + { HatchStyle.Wave, (8, new uint[] { 0x00, 0x18, 0x25, 0xC0 }) }, + { HatchStyle.DiagonalBrick, (8, new uint[] { 0x01, 0x02, 0x04, 0x08, 0x18, 0x24, 0x42, 0x81 }) }, + { HatchStyle.HorizontalBrick, (8, new uint[] { 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x08, 0x08, 0x08 }) }, + { HatchStyle.Weave, (8, new uint[] { 0x88, 0x54, 0x22, 0x45, 0x88, 0x14, 0x22, 0x51 }) }, + { HatchStyle.Plaid, (8, new uint[] { 0xAA, 0x55, 0xAA, 0x55, 0xF0, 0xF0, 0xF0, 0xF0 }) }, + { HatchStyle.Divot, (8, new uint[] { 0x00, 0x10, 0x08, 0x10, 0x00, 0x80, 0x01, 0x80 }) }, + { HatchStyle.DottedGrid, (8, new uint[] { 0xAA, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00 }) }, + { HatchStyle.DottedDiamond, (8, new uint[] { 0x80, 0x00, 0x22, 0x00, 0x08, 0x00, 0x22, 0x00 }) }, + { HatchStyle.Shingle, (8, new uint[] { 0x03, 0x84, 0x48, 0x30, 0x0C, 0x02, 0x01, 0x01 }) }, + { HatchStyle.Trellis, (4, new uint[] { 0x0F, 0x06, 0x0F, 0x09 }) }, + { HatchStyle.Sphere, (8, new uint[] { 0x77, 0x89, 0x8F, 0x8F, 0x77, 0x98, 0xF8, 0xF8 }) }, + { HatchStyle.SmallGrid, (4, new uint[] { 0x0F, 0x08, 0x08, 0x08 }) }, + { HatchStyle.SmallCheckerBoard, (4, new uint[] { 0x09, 0x06, 0x06, 0x09 }) }, + { HatchStyle.LargeCheckerBoard, (8, new uint[] { 0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F }) }, + { HatchStyle.OutlinedDiamond, (8, new uint[] { 0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x01 }) }, + { HatchStyle.SolidDiamond, (8, new uint[] { 0x10, 0x38, 0x7C, 0xFE, 0x7C, 0x38, 0x10, 0x00 }) } + }; + private void UpdateShader(Action action) { action(); - var size = 10f; - using var pattern = new SKBitmap((int)size, (int)size); + if (!HATCH_DATA.TryGetValue(m_style, out var data)) + throw new NotImplementedException($"hatch style {m_style}."); - using var canvas = new SKCanvas(pattern); - canvas.Clear(m_back.m_color); - - var paint = new SKPaint + using var bitmap = new SKBitmap(data.Width, data.Pattern.Length); + using var canvas = new SKCanvas(bitmap); + using var paint = new SKPaint { - Color = m_fore.m_color, - Style = SKPaintStyle.Stroke, - StrokeWidth = 1, - IsAntialias = true + Color = m_back.m_color, + Style = SKPaintStyle.Fill, + IsAntialias = false }; - switch (m_style) - { - case HatchStyle.Horizontal: - canvas.DrawLine(0, size / 2, size, size / 2, paint); - break; - - case HatchStyle.Vertical: - canvas.DrawLine(size / 2, 0, size / 2, size, paint); - break; - - case HatchStyle.ForwardDiagonal: - canvas.DrawLine(0, 0, size, size, paint); - break; - - case HatchStyle.BackwardDiagonal: - canvas.DrawLine(size, 0, 0, size, paint); - break; - - case HatchStyle.Cross: - canvas.DrawLine(0, size / 2, size, size / 2, paint); - canvas.DrawLine(size / 2, 0, size / 2, size, paint); - break; - - case HatchStyle.DiagonalCross: - canvas.DrawLine(0, 0, size, size, paint); - canvas.DrawLine(size, 0, 0, size, paint); - break; - - case HatchStyle.DashedDownwardDiagonal: - paint.PathEffect = SKPathEffect.CreateDash(new float[] { 4, 2 }, 0); - canvas.DrawLine(0, 0, size, size, paint); - break; - - case HatchStyle.DashedUpwardDiagonal: - paint.PathEffect = SKPathEffect.CreateDash(new float[] { 4, 2 }, 0); - canvas.DrawLine(0, size, size, 0, paint); - break; - - case HatchStyle.DashedHorizontal: - paint.PathEffect = SKPathEffect.CreateDash(new float[] { 4, 2 }, 0); - canvas.DrawLine(0, size / 2, size, size / 2, paint); - break; - - case HatchStyle.DashedVertical: - paint.PathEffect = SKPathEffect.CreateDash(new float[] { 4, 2 }, 0); - canvas.DrawLine(size / 2, 0, size / 2, size, paint); - break; - - case HatchStyle.SmallConfetti: - DrawConfettiPattern(canvas, paint, size, 3); - break; - - case HatchStyle.LargeConfetti: - DrawConfettiPattern(canvas, paint, size, 5); - break; - - case HatchStyle.ZigZag: - for (int i = 0; i < size; i += 2) - { - canvas.DrawLine(i, 0, i + 1, size, paint); - canvas.DrawLine(i + 1, size, i + 2, 0, paint); - } - break; - - case HatchStyle.Wave: - for (int i = 0; i < size; i += 2) - { - canvas.DrawLine(i, size / 2, i + 1, 0, paint); - canvas.DrawLine(i + 1, 0, i + 2, size / 2, paint); - canvas.DrawLine(i + 2, size / 2, i + 3, size, paint); - canvas.DrawLine(i + 3, size, i + 4, size / 2, paint); - } - break; - - case HatchStyle.Weave: - for (int i = 0; i < size; i += 4) - { - canvas.DrawLine(i, 0, i, size, paint); - canvas.DrawLine(0, i, size, i, paint); - } - break; - - case HatchStyle.Plaid: - for (int i = 0; i < size; i += 2) - { - canvas.DrawLine(i, 0, i, size, paint); - canvas.DrawLine(0, i, size, i, paint); - } - break; - - case HatchStyle.Divot: - for (int i = 0; i < size; i += 4) - { - canvas.DrawLine(i, 0, i + 2, size, paint); - canvas.DrawLine(i + 2, 0, i + 4, size, paint); - } - break; - - case HatchStyle.Shingle: - for (int i = 0; i < size; i += 4) - { - canvas.DrawLine(i, 0, i + 2, size, paint); - canvas.DrawLine(i + 2, 0, i + 4, size, paint); - } - break; - - case HatchStyle.Trellis: - for (int i = 0; i < size; i += 2) - { - canvas.DrawLine(i, 0, i, size, paint); - canvas.DrawLine(0, i, size, i, paint); - } - break; - - case HatchStyle.Sphere: - for (int i = 0; i < size; i += 4) - for (int j = 0; j < size; j += 4) - canvas.DrawCircle(i + 2, j + 2, 2, paint); - break; - - case HatchStyle.SmallGrid: - for (int i = 0; i < size; i += 2) - { - canvas.DrawLine(i, 0, i, size, paint); - canvas.DrawLine(0, i, size, i, paint); - } - break; - - case HatchStyle.DottedGrid: - for (int i = 0; i < size; i += 2) - for (int j = 0; j < size; j += 2) - canvas.DrawPoint(i, j, paint); - break; - - case HatchStyle.DottedDiamond: - for (int i = 0; i < size; i += 2) - { - canvas.DrawPoint(i, i, paint); - canvas.DrawPoint(size - i, i, paint); - } - break; - - case HatchStyle.SolidDiamond: - for (int i = 0; i < size; i += 4) - { - canvas.DrawLine(i, 0, i + 2, size, paint); - canvas.DrawLine(i + 2, 0, i + 4, size, paint); - } - break; - - case HatchStyle.OutlinedDiamond: - for (int i = 0; i < size; i += 4) - { - canvas.DrawLine(i, 0, i + 2, size, paint); - canvas.DrawLine(i + 2, 0, i + 4, size, paint); - } - break; - - case HatchStyle.DiagonalBrick: - for (int i = 0; i < size; i += 4) - { - canvas.DrawLine(i, 0, i + 2, size, paint); - canvas.DrawLine(i + 2, 0, i + 4, size, paint); - } - break; + canvas.Clear(m_fore.m_color); - case HatchStyle.HorizontalBrick: - for (int i = 0; i < size; i += 4) - { - canvas.DrawLine(0, i, size, i, paint); - canvas.DrawLine(0, i + 2, size, i + 2, paint); - } - break; - - case HatchStyle.SmallCheckerBoard: - DrawCheckerBoardPattern(canvas, paint, m_fore.m_color, m_back.m_color, size, 2); - break; - - case HatchStyle.LargeCheckerBoard: - DrawCheckerBoardPattern(canvas, paint, m_fore.m_color, m_back.m_color, size, 4); - break; - - case HatchStyle.Percent05: - DrawPercentagePattern(canvas, paint, size, 5); - break; - - case HatchStyle.Percent10: - DrawPercentagePattern(canvas, paint, size, 10); - break; - - case HatchStyle.Percent20: - DrawPercentagePattern(canvas, paint, size, 20); - break; - - case HatchStyle.Percent25: - DrawPercentagePattern(canvas, paint, size, 25); - break; - - case HatchStyle.Percent30: - DrawPercentagePattern(canvas, paint, size, 30); - break; - - case HatchStyle.Percent40: - DrawPercentagePattern(canvas, paint, size, 40); - break; - - case HatchStyle.Percent50: - DrawPercentagePattern(canvas, paint, size, 50); - break; - - case HatchStyle.Percent60: - DrawPercentagePattern(canvas, paint, size, 60); - break; - - case HatchStyle.Percent70: - DrawPercentagePattern(canvas, paint, size, 70); - break; - - case HatchStyle.Percent75: - DrawPercentagePattern(canvas, paint, size, 75); - break; - - case HatchStyle.Percent80: - DrawPercentagePattern(canvas, paint, size, 80); - break; - - case HatchStyle.Percent90: - DrawPercentagePattern(canvas, paint, size, 90); - break; - - // TODO: add other styles - default: - throw new NotSupportedException($"Hatch style {m_style} is not supported."); - } - - m_paint.Shader = SKShader.CreateBitmap(pattern, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); - } - - private static void DrawPercentagePattern(SKCanvas canvas, SKPaint paint, float size, int percent) - { - var rectSize = (int)(size * percent / 100.0); - for (int y = 0; y < size; y += rectSize) - for (int x = 0; x < size; x += rectSize) - canvas.DrawRect(new SKRect(x, y, x + rectSize, y + rectSize), paint); - } - - private static void DrawConfettiPattern(SKCanvas canvas, SKPaint paint, float size, int pieces) - { - var isize = (int)Math.Ceiling(size); - var rand = new Random(); - for (int i = 0; i < pieces; i++) + for (int x = 0; x < data.Width; x++) { - int x = rand.Next(isize); - int y = rand.Next(isize); - canvas.DrawPoint(x, y, paint); - } - } - - private static void DrawCheckerBoardPattern(SKCanvas canvas, SKPaint paint, SKColor fore, SKColor back, float size, int checkerSize) - { - paint.Color = back; - for (int i = 0; i < size; i += checkerSize) - { - for (int j = 0; j < size; j += checkerSize) + for (int y = 0; y < data.Pattern.Length; y++) { - paint.Color = paint.Color == fore ? back : fore; - canvas.DrawRect(i, j, checkerSize, checkerSize, paint); + int offset = data.Width - x; + uint mask = 1u << offset - 1; + if ((data.Pattern[y] & mask) == mask) + canvas.DrawPoint(x, y, paint); } } + + m_paint.Shader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat); } From afa0daac97381a76e6768afb069d1b19759f5ecc Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Sun, 4 Aug 2024 13:47:25 -0300 Subject: [PATCH 082/217] update hit threshold for image comparison --- test/Common/Utils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Common/Utils.cs b/test/Common/Utils.cs index e36ad3e..f2e13f0 100644 --- a/test/Common/Utils.cs +++ b/test/Common/Utils.cs @@ -70,7 +70,7 @@ public static float CompareImage(string filename, Brush brush, bool save = false float hits = 0f; // compare pixel to pixel for (int i = 0; i < bg.Width; i++) for (int j = 0; j < bg.Height; j++) - hits += DeltaE(bg.GetPixel(i, j), bm.GetPixel(i, j)) < 10 ? 1 : 0; + hits += DeltaE(bg.GetPixel(i, j), bm.GetPixel(i, j)) < 35 ? 1 : 0; if (save) { From 04c545d670456824ffec57ef783616ce49fd9d88 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Sun, 4 Aug 2024 13:49:04 -0300 Subject: [PATCH 083/217] enhance HashBrush test cases --- test/Common/Drawing2D/HatchBrushUnitTest.cs | 61 +++++++++++++++++- .../brush/hatch/style-backward-diagonal.png | Bin 0 -> 350 bytes .../res/images/brush/hatch/style-cross.png | Bin 0 -> 826 bytes .../hatch/style-dark-downward-diagonal.png | Bin 0 -> 268 bytes .../brush/hatch/style-dark-horizontal.png | Bin 0 -> 229 bytes .../hatch/style-dark-upward-diagonal.png | Bin 0 -> 252 bytes .../brush/hatch/style-dark-vertical.png | Bin 0 -> 295 bytes .../hatch/style-dashed-downward-diagonal.png | Bin 0 -> 281 bytes .../brush/hatch/style-dashed-horizontal.png | Bin 0 -> 417 bytes .../hatch/style-dashed-upward-diagonal.png | Bin 0 -> 272 bytes .../brush/hatch/style-dashed-vertical.png | Bin 0 -> 889 bytes .../brush/hatch/style-diagonal-brick.png | Bin 0 -> 955 bytes .../brush/hatch/style-diagonal-cross.png | Bin 0 -> 301 bytes .../res/images/brush/hatch/style-divot.png | Bin 0 -> 738 bytes .../brush/hatch/style-dotted-diamond.png | Bin 0 -> 475 bytes .../images/brush/hatch/style-dotted-grid.png | Bin 0 -> 495 bytes .../brush/hatch/style-forward-diagonal.png | Bin 0 -> 322 bytes .../brush/hatch/style-horizontal-brick.png | Bin 0 -> 671 bytes .../images/brush/hatch/style-horizontal.png | Bin 0 -> 236 bytes .../brush/hatch/style-large-checker-board.png | Bin 0 -> 846 bytes .../brush/hatch/style-large-confetti.png | Bin 0 -> 741 bytes .../hatch/style-light-downward-diagonal.png | Bin 0 -> 249 bytes .../brush/hatch/style-light-horizontal.png | Bin 0 -> 229 bytes .../hatch/style-light-upward-diagonal.png | Bin 0 -> 247 bytes .../brush/hatch/style-light-vertical.png | Bin 0 -> 292 bytes .../brush/hatch/style-narrow-horizontal.png | Bin 0 -> 199 bytes .../brush/hatch/style-narrow-vertical.png | Bin 0 -> 255 bytes .../brush/hatch/style-outlined-diamond.png | Bin 0 -> 742 bytes .../images/brush/hatch/style-percent05.png | Bin 0 -> 437 bytes .../images/brush/hatch/style-percent10.png | Bin 0 -> 586 bytes .../images/brush/hatch/style-percent20.png | Bin 0 -> 272 bytes .../images/brush/hatch/style-percent25.png | Bin 0 -> 269 bytes .../images/brush/hatch/style-percent30.png | Bin 0 -> 219 bytes .../images/brush/hatch/style-percent40.png | Bin 0 -> 258 bytes .../images/brush/hatch/style-percent50.png | Bin 0 -> 206 bytes .../images/brush/hatch/style-percent60.png | Bin 0 -> 219 bytes .../images/brush/hatch/style-percent70.png | Bin 0 -> 280 bytes .../images/brush/hatch/style-percent75.png | Bin 0 -> 271 bytes .../images/brush/hatch/style-percent80.png | Bin 0 -> 605 bytes .../images/brush/hatch/style-percent90.png | Bin 0 -> 331 bytes .../res/images/brush/hatch/style-plaid.png | Bin 0 -> 555 bytes .../res/images/brush/hatch/style-shingle.png | Bin 0 -> 952 bytes .../brush/hatch/style-small-checker-board.png | Bin 0 -> 263 bytes .../brush/hatch/style-small-confetti.png | Bin 0 -> 908 bytes .../images/brush/hatch/style-small-grid.png | Bin 0 -> 309 bytes .../brush/hatch/style-solid-diamond.png | Bin 0 -> 902 bytes .../res/images/brush/hatch/style-sphere.png | Bin 0 -> 833 bytes .../res/images/brush/hatch/style-trellis.png | Bin 0 -> 272 bytes .../res/images/brush/hatch/style-vertical.png | Bin 0 -> 893 bytes .../res/images/brush/hatch/style-wave.png | Bin 0 -> 658 bytes .../res/images/brush/hatch/style-weave.png | Bin 0 -> 598 bytes .../hatch/style-wide-downward-diagonal.png | Bin 0 -> 754 bytes .../hatch/style-wide-upward-diagonal.png | Bin 0 -> 851 bytes .../res/images/brush/hatch/style-zigzag.png | Bin 0 -> 890 bytes 54 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 test/Common/res/images/brush/hatch/style-backward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-cross.png create mode 100644 test/Common/res/images/brush/hatch/style-dark-downward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-dark-horizontal.png create mode 100644 test/Common/res/images/brush/hatch/style-dark-upward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-dark-vertical.png create mode 100644 test/Common/res/images/brush/hatch/style-dashed-downward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-dashed-horizontal.png create mode 100644 test/Common/res/images/brush/hatch/style-dashed-upward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-dashed-vertical.png create mode 100644 test/Common/res/images/brush/hatch/style-diagonal-brick.png create mode 100644 test/Common/res/images/brush/hatch/style-diagonal-cross.png create mode 100644 test/Common/res/images/brush/hatch/style-divot.png create mode 100644 test/Common/res/images/brush/hatch/style-dotted-diamond.png create mode 100644 test/Common/res/images/brush/hatch/style-dotted-grid.png create mode 100644 test/Common/res/images/brush/hatch/style-forward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-horizontal-brick.png create mode 100644 test/Common/res/images/brush/hatch/style-horizontal.png create mode 100644 test/Common/res/images/brush/hatch/style-large-checker-board.png create mode 100644 test/Common/res/images/brush/hatch/style-large-confetti.png create mode 100644 test/Common/res/images/brush/hatch/style-light-downward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-light-horizontal.png create mode 100644 test/Common/res/images/brush/hatch/style-light-upward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-light-vertical.png create mode 100644 test/Common/res/images/brush/hatch/style-narrow-horizontal.png create mode 100644 test/Common/res/images/brush/hatch/style-narrow-vertical.png create mode 100644 test/Common/res/images/brush/hatch/style-outlined-diamond.png create mode 100644 test/Common/res/images/brush/hatch/style-percent05.png create mode 100644 test/Common/res/images/brush/hatch/style-percent10.png create mode 100644 test/Common/res/images/brush/hatch/style-percent20.png create mode 100644 test/Common/res/images/brush/hatch/style-percent25.png create mode 100644 test/Common/res/images/brush/hatch/style-percent30.png create mode 100644 test/Common/res/images/brush/hatch/style-percent40.png create mode 100644 test/Common/res/images/brush/hatch/style-percent50.png create mode 100644 test/Common/res/images/brush/hatch/style-percent60.png create mode 100644 test/Common/res/images/brush/hatch/style-percent70.png create mode 100644 test/Common/res/images/brush/hatch/style-percent75.png create mode 100644 test/Common/res/images/brush/hatch/style-percent80.png create mode 100644 test/Common/res/images/brush/hatch/style-percent90.png create mode 100644 test/Common/res/images/brush/hatch/style-plaid.png create mode 100644 test/Common/res/images/brush/hatch/style-shingle.png create mode 100644 test/Common/res/images/brush/hatch/style-small-checker-board.png create mode 100644 test/Common/res/images/brush/hatch/style-small-confetti.png create mode 100644 test/Common/res/images/brush/hatch/style-small-grid.png create mode 100644 test/Common/res/images/brush/hatch/style-solid-diamond.png create mode 100644 test/Common/res/images/brush/hatch/style-sphere.png create mode 100644 test/Common/res/images/brush/hatch/style-trellis.png create mode 100644 test/Common/res/images/brush/hatch/style-vertical.png create mode 100644 test/Common/res/images/brush/hatch/style-wave.png create mode 100644 test/Common/res/images/brush/hatch/style-weave.png create mode 100644 test/Common/res/images/brush/hatch/style-wide-downward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-wide-upward-diagonal.png create mode 100644 test/Common/res/images/brush/hatch/style-zigzag.png diff --git a/test/Common/Drawing2D/HatchBrushUnitTest.cs b/test/Common/Drawing2D/HatchBrushUnitTest.cs index 340634a..dc08e27 100644 --- a/test/Common/Drawing2D/HatchBrushUnitTest.cs +++ b/test/Common/Drawing2D/HatchBrushUnitTest.cs @@ -1,3 +1,4 @@ +using System.IO; using GeneXus.Drawing.Drawing2D; namespace GeneXus.Drawing.Test.Drawing2D; @@ -10,9 +11,61 @@ public void Setup() } [Test] - public void Constructor_StyleForeBack() + [TestCase(HatchStyle.Horizontal, "style-horizontal.png")] + [TestCase(HatchStyle.Vertical, "style-vertical.png")] + [TestCase(HatchStyle.ForwardDiagonal, "style-forward-diagonal.png")] + [TestCase(HatchStyle.BackwardDiagonal, "style-backward-diagonal.png")] + [TestCase(HatchStyle.Cross, "style-cross.png")] + [TestCase(HatchStyle.DiagonalCross, "style-diagonal-cross.png")] + [TestCase(HatchStyle.Percent05, "style-percent05.png")] + [TestCase(HatchStyle.Percent10, "style-percent10.png")] + [TestCase(HatchStyle.Percent20, "style-percent20.png")] + [TestCase(HatchStyle.Percent25, "style-percent25.png")] + [TestCase(HatchStyle.Percent30, "style-percent30.png")] + [TestCase(HatchStyle.Percent40, "style-percent40.png")] + [TestCase(HatchStyle.Percent50, "style-percent50.png")] + [TestCase(HatchStyle.Percent60, "style-percent60.png")] + [TestCase(HatchStyle.Percent70, "style-percent70.png")] + [TestCase(HatchStyle.Percent75, "style-percent75.png")] + [TestCase(HatchStyle.Percent80, "style-percent80.png")] + [TestCase(HatchStyle.Percent90, "style-percent90.png")] + [TestCase(HatchStyle.LightDownwardDiagonal, "style-light-downward-diagonal.png")] + [TestCase(HatchStyle.LightUpwardDiagonal, "style-light-upward-diagonal.png")] + [TestCase(HatchStyle.DarkDownwardDiagonal, "style-dark-downward-diagonal.png")] + [TestCase(HatchStyle.DarkUpwardDiagonal, "style-dark-upward-diagonal.png")] + [TestCase(HatchStyle.WideDownwardDiagonal, "style-wide-downward-diagonal.png")] + [TestCase(HatchStyle.WideUpwardDiagonal, "style-wide-upward-diagonal.png")] + [TestCase(HatchStyle.LightVertical, "style-light-vertical.png")] + [TestCase(HatchStyle.LightHorizontal, "style-light-horizontal.png")] + [TestCase(HatchStyle.NarrowVertical, "style-narrow-vertical.png")] + [TestCase(HatchStyle.NarrowHorizontal, "style-narrow-horizontal.png")] + [TestCase(HatchStyle.DarkVertical, "style-dark-vertical.png")] + [TestCase(HatchStyle.DarkHorizontal, "style-dark-horizontal.png")] + [TestCase(HatchStyle.DashedDownwardDiagonal, "style-dashed-downward-diagonal.png")] + [TestCase(HatchStyle.DashedUpwardDiagonal, "style-dashed-upward-diagonal.png")] + [TestCase(HatchStyle.DashedHorizontal, "style-dashed-horizontal.png")] + [TestCase(HatchStyle.DashedVertical, "style-dashed-vertical.png")] + [TestCase(HatchStyle.SmallConfetti, "style-small-confetti.png")] + [TestCase(HatchStyle.LargeConfetti, "style-large-confetti.png")] + [TestCase(HatchStyle.ZigZag, "style-zigzag.png")] + [TestCase(HatchStyle.Wave, "style-wave.png")] + [TestCase(HatchStyle.DiagonalBrick, "style-diagonal-brick.png")] + [TestCase(HatchStyle.HorizontalBrick, "style-horizontal-brick.png")] + [TestCase(HatchStyle.Weave, "style-weave.png")] + [TestCase(HatchStyle.Plaid, "style-plaid.png")] + [TestCase(HatchStyle.Divot, "style-divot.png")] + [TestCase(HatchStyle.DottedGrid, "style-dotted-grid.png")] + [TestCase(HatchStyle.DottedDiamond, "style-dotted-diamond.png")] + [TestCase(HatchStyle.Shingle, "style-shingle.png")] + [TestCase(HatchStyle.Trellis, "style-trellis.png")] + [TestCase(HatchStyle.Sphere, "style-sphere.png")] + [TestCase(HatchStyle.SmallGrid, "style-small-grid.png")] + [TestCase(HatchStyle.SmallCheckerBoard, "style-small-checker-board.png")] + [TestCase(HatchStyle.LargeCheckerBoard, "style-large-checker-board.png")] + [TestCase(HatchStyle.OutlinedDiamond, "style-outlined-diamond.png")] + [TestCase(HatchStyle.SolidDiamond, "style-solid-diamond.png")] + public void Constructor_StyleForeBack(HatchStyle style, string expected) { - var style = HatchStyle.Horizontal; var back = Color.Red; var fore = Color.Blue; @@ -22,6 +75,10 @@ public void Constructor_StyleForeBack() Assert.That(brush.BackgroundColor, Is.EqualTo(back)); Assert.That(brush.ForegroundColor, Is.EqualTo(fore)); Assert.That(brush.HatchStyle, Is.EqualTo(style)); + + string path = Path.Combine("brush", "hatch", expected); + float similarity = Utils.CompareImage(path, brush, true); + Assert.That(similarity, Is.GreaterThan(0.95)); }); } diff --git a/test/Common/res/images/brush/hatch/style-backward-diagonal.png b/test/Common/res/images/brush/hatch/style-backward-diagonal.png new file mode 100644 index 0000000000000000000000000000000000000000..f8f5d0e52e9b5e4dd407075cc1a66da3bf40ea55 GIT binary patch literal 350 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;Ac-4$B+!?w^I-D9&+GeSX z4;#tnxPMakxv^_*WbYyN_0Ox`Zv7Vi<=!0m+XloR5HqLIEG+AIr8}duQKE z=hw>zO%?d@b-nBV6Y)QkYL4js5#wyy-6S+!V27rLB~ZDlzTyYtD=L3$-f{Gvmwi|m z9`Hzr$(&VHU9n*E6IGVFwpmN&pO;CJIRWXhbD17{~q7x*EWHK`6Le(ZVQ2dwUT-gn{09YMKrR#m;1&#LPg YK5i>~`!P&K5$JsePgg&ebxsLQ0E#}39RL6T literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-cross.png b/test/Common/res/images/brush/hatch/style-cross.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d87145e89ad0b4a85bf939246c72cbbde44194 GIT binary patch literal 826 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^2B!0#E{-7?&UgC{b}~Eg9Jy3~+44#IMXnf! zcZ!BD%;&#<{`oKenTcE2ucQUccFbklDtRUC!W&uk6=xTiHQ(aNG6phkTJyo>fQ-%M zG6*@d=Ct<~4iJ4{MQ867HbC`(44Ad|F$2g!lvI6;>1s;*b3=DjSL74G){)!Z!;6hIq$B+!?x0en1S_~MN5B~Xoc}~OP4cyn` zPO?r&TrFaE-SYb1KlbmrUrJck>3w?m_u!Y}Ums*Ah*h{)y7g_JXvJ-_NJjbiZI8LE za)EslALlMP$JDQJ{KQAIp!7!fDalVNq*rb}pnS@(Xix8z$OnQ?B`oi`z0QPd-o huUtyN7>|!T>%PaAPW#X*a1H2K22WQ%mvv4FO#mheXPp25 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-dark-horizontal.png b/test/Common/res/images/brush/hatch/style-dark-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..ef01405d6288699f032f79b778b99605f75ee5c0 GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V3ntfV@QVc+sg|D85lSYJA8|m5<9`b8~#yk z-J;FLzYoq=ys*T8tMKbf_fH8Tk6vBwe{xXc$kpZYCz}I1R{PseVqYu}KEF!lvI6;>1s;*b3=DjSL74G){)!Z!;6zUs$B+!?x0g5a9xxC%eBge4rh1{E1S7iu zvuOT<8#^zYS@ZQy-Mh_I^-rXG>q_008fx`ke4eLrPGah@7scj6={;V_Uv^4&8Utm1 z-RV6dx%iSq?K`(eJ%O1$cIC>2$5xoQ&EGxIA~{5A@%g(RbMR^ZGr9N7E{4y^rPHiM S8V&#*$KdJe=d#Wzp$PzOOJG+3 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-dark-vertical.png b/test/Common/res/images/brush/hatch/style-dark-vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..5a62da4ad9bcb24232d76ad786815550e56c36ff GIT binary patch literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;2uvG$B+!?tDOgV84NgD?$l?_UdGFB_TV9B zVp-bzzu%X=XmCEu*glJy&y-yzi_hYUOu-7v2LZ(g7CswbiBswPkvyf-TvyGT3G@ep Mr>mdKI;Vst0R4Ano&W#< literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-dashed-downward-diagonal.png b/test/Common/res/images/brush/hatch/style-dashed-downward-diagonal.png new file mode 100644 index 0000000000000000000000000000000000000000..6c7f5904f5add54870bad768c64c4ecf578175c6 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;5tti$B+!?yO#~QTmpFxAE^KDz5i$|liJ^$ zg$+-v?tkC=UH%HkB!lvI6;>1s;*b3=DjSL74G){)!Z!1~X3=$B+!?t0y<|9%0})eBgflOPk#-E#d)= z2h{)1yZ`>Dn3M<%)_t=nvp#+`IhyL!eNx%o^{E02usn^Ye|jGU7nh zMjUYcQeWoXYX=*!L2scW3_eUhpS-;|=8cT_X$83cj0@FO^6x+f9Ng6cx2ko%ZU0@6 zT`@Y%4lw--9pAsK-2pOy{k0IxsZt`ebL{5lKbemhluj#X{B{o*tPGy6elF{r5}E)A COrP=q literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-dashed-upward-diagonal.png b/test/Common/res/images/brush/hatch/style-dashed-upward-diagonal.png new file mode 100644 index 0000000000000000000000000000000000000000..201ddb0c86ca9780ab97aed6ac003f337a1a79f5 GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;8IT)$B+!?yXQA@x)}1X9=u!lvI6;>1s;*b3=DjSL74G){)!X^24+4_7srqc=h9BbE@lUwmS6wK-(XwB z^X&8YnbwZEY+EI-qy@|dGS1dLW`J|HN}BxcWroUtL@&HC=L0K)D%pMhcs_!Eds4$K zo-E@PXBU_?0~ygm2q~b^-b@H7;}y9F;sYVdAnMch7b0t(_4gPflJbmi((Eu}LF(^5 ze{7E6=XyFI`CzLqCqnA%f^DjdNdCEE_F%mRva>edw?NiR!29n$f0RZ9$2JvYAKaS8 zf{;oJKno1%58)!n&bpZ|gRGf=_rL%A!HWnE`~iy^7`#8UxsaTN9)bA1zvXw$A4Jl? zAFybF!S+X#72#rI^a#Z7{adwmb)XakO-gtJ7A-KC|8+4T#W#BTeg66Tcm5*=rPH)n Sk9PyJ3xlVtpUXO@geCwj7kqdC literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-diagonal-brick.png b/test/Common/res/images/brush/hatch/style-diagonal-brick.png new file mode 100644 index 0000000000000000000000000000000000000000..1873ba272b507f6de639f9b82518b1662728acea GIT binary patch literal 955 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^2Ic@y7srqc=hE|zlMWm3FdzK;fBEKC4V|rm z&kT|aN@aA0=czTS@-ChUEZW-}8f!-?HBZd=}h zJP-A?zP(%yI4~gmz1tTetDY?R{IIhy!h`0W@6I-(Sdd;V(~cD6o>$(!c1H~p=HooG z&ulmeCt~fCZ+!!K9_s7q^89Atz<}`Y-M)aV`ee`NhYL~s{Qa3DvIQ46+x9ubybn$; z-@YoNhDl>GO0?e4n=hCPO#)zFpYCU$jl_SSn*mQUpu}_R=Z6a@ey;wkfNa5wn{vk$ zkz=X!_pgblVRFC_C0cW)pYPZPO#)zFKRw;)yB`yP8IHlz)z4*}Q$iB}^kvYk literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-diagonal-cross.png b/test/Common/res/images/brush/hatch/style-diagonal-cross.png new file mode 100644 index 0000000000000000000000000000000000000000..3190855b71f8ec491604c89c43541f8c94a540de GIT binary patch literal 301 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;2}>J$B+!?w^t8xG6f1SU&u^)y|Lu=#xDJM zjwAJQb5l+x7M|p0uAXpZ^}4Ssp0A3EKYwo9O$#Qy;%AS(u0H(#+xhl_pE^5#?6qTA z`uFsE4a*<3|K6m()VW=8dts*gfz)#`Hr9)t-4rxE-*=*t20}J}qVm_7!lvI6;>1s;*b3=DjSL74G){)!X^2BvaP7srqc=ewN;dsz*6TI>G%p6803C7{Ne z!mnWe=l9=v&+8X7?%~cdUU7DTS@SKPtY{So4HaPHeG(fm+cB4ItK^lmfZLBG^jYc` zIY7A}@w0Q=VX`3Z-fC%fg!irfnh_gKMZgDuhR`{?vR zoeO9c%+3pUDkYG)ySEoIAk<5ml*@fzH0lAQ2pRYoke>IPpXWvCv{qfm?Z9-&;OXk; Jvd$@?2>|=TE~5Yd literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-dotted-diamond.png b/test/Common/res/images/brush/hatch/style-dotted-diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..9beb47e5d9361694ab72b406e91b3c02886331af GIT binary patch literal 475 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!h5}C)$B+!?yAu!QJ}}@pu$U-lz@)_H7b;adsGqz2 oekVtrYvZUM^e{;K^!qnI)2GsD69m^a03(FK)78&qol`;+0Gsd81poj5 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-dotted-grid.png b/test/Common/res/images/brush/hatch/style-dotted-grid.png new file mode 100644 index 0000000000000000000000000000000000000000..35e1b109c41127f599b569355ea38d97582a4a1a GIT binary patch literal 495 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!h89m3$B+!?x04TcG6(V;y;OfWa!Z=S2~i6k zmtXupC7yl$`{)0@>-W#UU_YSEbtNrewqq{aR>>=A7v9L6YYG=xadv@O^DUk%V<6+E zzC>H%qYttP7 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-forward-diagonal.png b/test/Common/res/images/brush/hatch/style-forward-diagonal.png new file mode 100644 index 0000000000000000000000000000000000000000..9b56c7d51b80e3f81bac4321a8fb54d3165d08bf GIT binary patch literal 322 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;B`+I$B+!?w^vVdIyv&RKFoY{E&1iOWKreo z3nW+-`7QQOa21=WFJrWN&26i1sj=Uqey=|FegDS`1~;x(2lEF1daQiDZQmRH`r0RJ zvSwVfm&;1@D(?-o^Lep0(aT*o7^Lud--ViY9Oq+`r}7I|WzV=2csj(!gy-8%Ws~{V zhl;O9JU!fUPik4o6(do8-sakO9A}^ReYj&9_S0bD_BKt~tv*Mc4=2pGcKW!lvI6;>1s;*b3=DjSL74G){)!X^1|}0v7srqc=eJWFyI2f)jy(H6f1Ya|N7oA* zh0`)BTz6|eKL7o*=09_>$1S!j;}vHYm^I(x$uiFPCdD2w+cB4ItK^k5AY=1&iw2N5 zRKcvhSG#|1u`SGp$paPIszCKZ)W``T>(4!0y|>>|C6^g$9>`#m>Rx8Jg+Rv5Y#9f* zg}H2J@4Y(u=iYlxu>F|kw|%dYC&STu?+e51toA^*ZY%HmuXfKSY~h932lV)bH>P~Z zo=RI^$bjO3)#9J4_p8G^`fUPo7-dP{dtG}*F;g05A1Lt9{TTO{0UoHJP!H38_Px#@ zX6IW)MkMR5nC-d#dxrBRGnjo?Bjy+*iU-!loq7Lv9?YY+otcoVTajC_{<~rOl3bX5 gpxC#Y|Nc9_mSE|$qkbRQfr*O2)78&qol`;+08q;ywEzGB literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-horizontal.png b/test/Common/res/images/brush/hatch/style-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..0256fd3805e9e2223a923eab2481a699254ee4d1 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V56stV@QVc+e;e-4><4~-f+J@Go6ty;YrMb zMD4wm`TricI;V=jAxr7kJ6pe<)Hnr!9IsyO%zisLa0&zpUb$L){g(P-4G2_TvHJPC wt^RI-P#6$?J~n%P&mt&Xux{Rb9iz{TUl>ZKwMy6&0Ug8O>FVdQ&MBb@0Mxuy=>Px# literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-large-checker-board.png b/test/Common/res/images/brush/hatch/style-large-checker-board.png new file mode 100644 index 0000000000000000000000000000000000000000..99b9a09debe71fdf53b1c4a7ca31409280af0d5f GIT binary patch literal 846 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^2BycJE{-7?&ZQ?EyN){W96fO7|E2nr8C#;8 zbXe9sfBxNO|LzM5V6dQkt?>-DRv6r!J@=rA1`OVbea*AXaUm2w+*)sQ&(zkSII z$bi8M^X~C-!QrjTyWzSPIzHcXToY!Zi3XaP@1oKT;QF}AuUj=|!psb)cq`w(9p=yr z3vBmFi%7xXmgVJempC<}y2bK+A;K+aX71R1P0|Oh^U&R`=Mvy%ip2ds{#Y08)C~Xk zuwVhwZ(rVp2O64NO3qc!frW}wGn$$2)|IkdhU-KN?(6?PR&c|;j~Z;%Rp#)BFhO<8 q+|P2$5#fSn=CF^#AN4l-?SICWPMfp6=KwGdFnGH9xvX_PDW literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-large-confetti.png b/test/Common/res/images/brush/hatch/style-large-confetti.png new file mode 100644 index 0000000000000000000000000000000000000000..c21d9d5710a78b870d2b4d3f394f9ccf6d92ba31 GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^2Bs=c7srqc=eJiKyN(+09JsLa|I+^sD=tk} zV}G$IKvFS(kLJ(ce|z7XAA6d3`S7xKU;f#0X13SLdaOHrg=VXmdFD*I5!UzdZ@YT= z>`i)KH@t+YvNL!AV*)j#%{~+aQ?9bnbJJgCgm#sUyz>kzt8RROX_hbU7B{oafr=#O z9^Wzf22fMd!lvI6;>1s;*b3=DjSL74G){)!Z!V4tUpV@QVc+slSr42mMm7wi9fYv(EMIC@2r zW5tIVr<687v;JPydoiU)?tcHAYPq%Ye-@vT@ICgT(0o#Qk6QAV9nvR_-6k7W-RXTI zx%i}n?K`(mJs{QP%9Y15jNJNnPqaxkkz9QIuE#vXSv?mY=c$~>sr~3?Gbz^6X*shx R9|K*+;OXk;vd$@?2>=GuUOoT- literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-light-horizontal.png b/test/Common/res/images/brush/hatch/style-light-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..e277a271557b934c946a21d94654fdaa0cf0555d GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V3ntfV@QVc+e?Om3=RT^4t&m^WiG(@q&;c& zmV%w9`T1svxGDEI#nhIqwUcPw@#{@gtpQiz*PB~^C5SwFwK@CiL5(9<)33j34(wQc ye%)2}#RB2;V^{OLDXz2I9VU-c!lvI6;>1s;*b3=DjSL74G){)!Z!V2`JZV@QVc+shj{84LxE9C*F|Wpz%$rxgvN zISGd~b|<8#ec$`JviIWi>J#=_y@8oMZpmMEN}n`#TWt92PVW=R#TO-N-?@G2xtP&o zSFT)nEW^ZY{_cr3$tF^Z&)@Z!XD9{Knx}GJqW9Q~V)IGqc(j|nuM^`eoyHOK#{=jt N22WQ%mvv4FO#t=TVDSI| literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-light-vertical.png b/test/Common/res/images/brush/hatch/style-light-vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..c0b64e2a4603f66e77558fb41ea1376a7a721ea0 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;7(5$$B+!?tDOfq84LuP^ZqXlesQ>6nuAm0 z-n;&E*@6|84+4r0EPU4Be3r3&7BinIyG$0J#TA)>mGJoajluJ3>9jd#?9T!{!QkoY K=d#Wzp$Pz`l4V2y literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-narrow-horizontal.png b/test/Common/res/images/brush/hatch/style-narrow-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..90184420da07fb42eba97a92c607c95bc7d91c9b GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V2r1WV@QVc+lz*R3<^944*W8YUj3 zbhGjA=AB#2@4l{eN8N#cIEx`M4&wkp00i_>zopr0LuMD AX8-^I literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-narrow-vertical.png b/test/Common/res/images/brush/hatch/style-narrow-vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..2e207b3d22169b10318b171174121ff315c6fb7a GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;1o|6$B+!?tEUVZ4><4~+3-QVww6Qg>d~m0 z^4oF$^5sfDubAzA*XGr;K=b}xl~>L#&ZD(ZYQFr}#L{WAw@gU`x{txr)z4*}Q$iB} Di4bb= literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-outlined-diamond.png b/test/Common/res/images/brush/hatch/style-outlined-diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..80ed5abf808824e489dbbe98d8cfdeb555dfd9d5 GIT binary patch literal 742 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^2BvCH7srqc=ev^+7PcA)9DT9>C5x)WmLgUI zVb3?G_y77~`R||EglIUdKJ$2f`MsadvmEoF@LeC@lidsQ*h-C8%yzuX^Xgkct-^LV zEPpN%FBe}~9s?KIu28;bV$&|kD`#PvzT5IO-f4ov_j@K*zOj#jYbbYT{}dZw-VD=H zy~7UfoahO=i@Wsu*YB~`LlU#T=K*p_8cfq~xpoJbbMqYEmESvI^G#kCu3`6L=AU{H z$Ah)l#>k)0g~Pj+N2ebTzb7jW7tuW-o4*9+c#u68YWtnx&fTu?-8w%-?svZ!T*KXq zjWyy3|K62b$NxkO4&Tjrbo#OO`#x^Ch}e@p^B|bxK_UBM-*H8_bE7Bxo@;K@|JNO% z;oS>|ecTBD=J8)=FXD#7a=(vHKTa(_&I%XdE;=r)iR5F+lKYPZ;Lg=O@%Nnc%;WzS zBQ%tkDBNd-#c*1{_V(AzJ6Yk-dijq}KThp_%mf!<-TAmz1j)xdyWSUaAo;KENw19k Z{;sytY01(}Z-Hr(!PC{xWt~$(696!lvI6;>1s;*b3=DjSL74G){)!Z!1}{$+$B+!?t0xa;CO8Tl33xt#i}>4^mOAT( zrb^evKY#xH`TyUMEs~ehd}bTw9@`@M`CZK$tH+Cs^}G^p^<)|^J3GTH8OW$Ux8p@m z_c@D>zb=VL;+5rgWpfKJN{Xu*fHcEQu}zODIp&%!bEIw&viKfr`Q2c1gdm1OO`m71 z2eOeb$vzNS{NCLDSg<*qP^UpmKQAc`vXOn`d<|sr_venU1DnGFb27;EZO?&@2D-fY zhQA21c=_|>>mYLinP5%^x&3xA(9uAbJHAopLJ~i-`}4+^-@nU0{!%*4UEA{-FsK!lvI6;>1s;*b3=DjSL74G){)!Z!hWnl_jv*P&S33`OvIOw7F8jYMy|7sD^YR9V zDeXUN?EilkzLFL&+cB4ItK^lm3vX=RO=`HslV!Z(>;ki9AVYfIM}-R4MkJwGbC1Ug z!F7NbhmGY})@vXOZT`H&n+aiv@rvAvW6cjlS&@V?%H+yD9U#_1wDO*>5ZJQ_S?KJ! z$9kMFmw=qK?V%)pQ@9AS(9Oy_s*E7F!|XS+dF=S1iwQ}{#JYdC3dB25f3>CCaKr^7 z3#C0ToX&zUBrV|fhaUEW+FVFNgD-H@?!T9>PcNP3cKfdbF#Z`lUHx3vIVCg!03e(3 A+5i9m literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-percent20.png b/test/Common/res/images/brush/hatch/style-percent20.png new file mode 100644 index 0000000000000000000000000000000000000000..6e678b992c3436b27b5f92af4b208187f2caac0e GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;8IT)$B+!?x04;Ym>mU}zW<+pFJZz9;WZZw zx!wcfXGQBQK@jd(-2ET~9vVJs~$e<+!ux$;Z1V+UlL`YuB1|yj-~& hYC28>@9cTczRIg~TKP4#r$7fYc)I$ztaD0e0swKPY`Opd literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-percent25.png b/test/Common/res/images/brush/hatch/style-percent25.png new file mode 100644 index 0000000000000000000000000000000000000000..6b4816a4435ef71c530008b496d7be8f8f72ab63 GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;37{K$B+!?t0y*cGB^qxKJa?~%i4es5+RKT z_}t#h{l34|>2ikibuQn!lvI6;>1s;*b3=DjSL74G){)!Z!V1cKLV@QVc+shX@85DSqTzI#?gmr0$^b?hY z$}RgRy}Gsj^4fAs;hpCV@BURb{w}fO{G@fg7q$q_GIP4@mT`;2S9)QlvPo{sGGYXO cdMaP#Zr@xw&2LWlBB0$2p00i_>zopr0K&gcTmS$7 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-percent40.png b/test/Common/res/images/brush/hatch/style-percent40.png new file mode 100644 index 0000000000000000000000000000000000000000..4d36f93b64315f4919c7c7a69a62c88d76d20277 GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;B-$H$B+!?w^tpxSPXfX5B~XoInv-uqO05m zITmHEr&b*EtDo(@ZM**XR6BX`Uw@Qa<_T4Kz0h2^9LV_fBV6ou?~OV3P8B|!wyIyI wTnKjZ=d_*sl73f3;Pgg&ebxsLQ0HcOmr~m)} literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-percent50.png b/test/Common/res/images/brush/hatch/style-percent50.png new file mode 100644 index 0000000000000000000000000000000000000000..267317a023a2d4ec1234fb38baa9c3c3a7b7f91a GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V6vx+V@QVc+lw1H84LwjHoo5fviipj)}G!3 z#|gTe~DWM4fIax)o literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-percent60.png b/test/Common/res/images/brush/hatch/style-percent60.png new file mode 100644 index 0000000000000000000000000000000000000000..0312c41728e2bcf293182840fdc0da3dbc71b916 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V1cKLV@QVc+shj{84P%i9JpVf*)GB_>By(m zW4YaJW{OpLZfV!o->&J4|Nrt>e!O>^;H=!1H0gym-7-jKKJ&MmWSJ^cI?cjz);^%! N44$rjF6*2UngELwOP&A# literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-percent70.png b/test/Common/res/images/brush/hatch/style-percent70.png new file mode 100644 index 0000000000000000000000000000000000000000..ec2c77f75ab36407550013d79339017470f8976b GIT binary patch literal 280 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;95@?$B+!?t0xX}vN-Y_IdIZGQ@;D1;tP&1 z8(v!lvI6;>1s;*b3=DjSL74G){)!Z!;1W+4$B+!?x04U@9&q3}l=pw>dZ%0^w*#X5 z?0=q2EIzk4{CV@M{NIe$E{3u%^-_K*XTA|QTC!yKgqQtGHad%3p7_pbnf;_=e1Sgh jySQim^N^ytLb2y|4(+AW@-|t6yvyL}>gTe~DWM4f9~NjE literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-percent80.png b/test/Common/res/images/brush/hatch/style-percent80.png new file mode 100644 index 0000000000000000000000000000000000000000..66dce3409009836d81eaff877b58f5a578591871 GIT binary patch literal 605 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!hEJX@jv*P&cl!!lvI6;>1s;*b3=DjSL74G){)!Z!-~&$=$B+!?w^KLvvO4m#Zu`IN`;pwH8Ek*t z`2>Xe;+{OOo%fxs?1V|5;)=5i%$jfUWEq1PKlH>mI4}G7zzr?}Wc=FxSbwwgvO*rm zT(+%}SJDDzgBSr2ayz^nr4l&+c14YcTny-}}so6UQ9k@Ka~f*(Hy1ptGm LtDnm{r-UW|HXDR^ literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-plaid.png b/test/Common/res/images/brush/hatch/style-plaid.png new file mode 100644 index 0000000000000000000000000000000000000000..e6bb641d2a932feddc530827b3bde452a26f7ef8 GIT binary patch literal 555 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!h6A21jv*P&Z?6Vs9Wf9&dg12(Wy^~^DyF_* zdX%){lg0P#2LEd8uir2K{`+V4olpNP^D6#duYXd$&=Ce7zKOb?ASD8W=hl_-F2gBV zSNksUdG)=C+wXqvvnl1^$})4FSr1O1!Zfq4ImO&B)`2b=C{88xo^+gdF4-5 zI%focpvCTMrZd=DVQ_PHY;z`90-I#~{OTJ&ftqu_?@{)bb!rB|LpQgsJ7}T-gL7hE zOZk8$uu1;?b?5LopyvGD&t)rL2|zrK-`i+ma{akn**>7=+qUmd1h+#xj^Ep8VY1)u j`-abd)%Qouf6uS#P&)0&27{Zx2xjnf^>bP0l+XkK{aWR+ literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-shingle.png b/test/Common/res/images/brush/hatch/style-shingle.png new file mode 100644 index 0000000000000000000000000000000000000000..24af73c3ac28807a68c4f772d5fc7877bcf5284a GIT binary patch literal 952 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^24-JR7srqc=hBmbeaenJhcE2?zcgL3ofmf6Z1OOhsc`2rbx-dfq+__Fq8^yTf#@_p~$`Z&M8+4ugeeWtb9 zzpj1RU1;<0YByLRSjXG676ivvYg_yJ4-;nk8g2GWGdp=kZRSmt;_a_@Kv>4JQ_?1% z$(dwn-Vc|Bi0!QOII1Q(J0vG)qvuhzsoP_Zv(JXw2Xu$^zt4pmV9uFUXWpm2rz#Mn z5TfI5PXL1BdGy-{Q8^^*ffl?glQjoA1ZF+ZbD_gY)sU>;?D_1D4Jd4o#GXG> zga-hSaj#5PJ_i`w&}ex3JZVnXwasqb7R$aCpTDaNxr% z*m=A34kYklaWiN3ab)YkPJ*QXP{f|Pz3wqmw6B%@|I-4ID)vhs7mnMz0-k1W!lvI6;>1s;*b3=DjSL74G){)!Z!;2cjE$B+!?x0erc9&iwFy7+egOBS(5j?y34 z6}jzfpBg;3{J#4=w~zb5FZFdtxYvl*9I)hCR<2wc_JQ%a#*BNtS0WFx&sm&MWge8y nDqqmo^zyF9TgTe~DWM4fn|WgS literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-small-confetti.png b/test/Common/res/images/brush/hatch/style-small-confetti.png new file mode 100644 index 0000000000000000000000000000000000000000..e87427bdc83b527e5667576376d6819913231226 GIT binary patch literal 908 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^24+Q17srqc=h7*GT~7=Ij{f>LeO_= zQH$pNxjEtf{r7)k9dp^XN?u6|m@rttx%$jeB_3!w$1BuUL^>5;T zkOWM{w`GlcxM8Y!vVKcEW`diwRr1SQ!+8kR+vX}RLehHcUXe>9+@M=LQQyBNA{??+ zSFZ4FAv=;`C4aLPB55DBc{H>sOJRTO>g)eAtnw!lvI6;>1s;*b3=DjSL74G){)!Z!;3-cR$B+!?yZsw^SsVopt^2?1yDHO>9nAF& zExt#-?|c8(xhQ4sv}lVPJ6fB2`$g5=uVwB8vVa0VA_@nO)a3iW89Dq*r)90sF9Z6D N!PC{xWt~$(696=8euDr2 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-solid-diamond.png b/test/Common/res/images/brush/hatch/style-solid-diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..d0cd11e024283d3fb23778d28c942a66759cd8ec GIT binary patch literal 902 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^24-nb7srqc=c^|!mL@auFkO7R|7FgG)VMQ@LT%Et<*W@dk1p`r4$o@P4)-2w-Q{#+lBGaS` zv!TX&F0%Lh^8LftKivwPdp*CXcm7j>+RkrWGZShjNSn`(Uj6qMm7A+l0$^tA+xt&^ zt&0P7-eR>GQ!rZ-&%Qg4u2>EeH$vUo&&8kL&w(Ba28XAEg9oau z<^GRPauEbk8F+6wuvwjnLD2((dF8=ZJC;#mE VrPJ1YX9VRP22WQ%mvv4FO#rWmolyV) literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-sphere.png b/test/Common/res/images/brush/hatch/style-sphere.png new file mode 100644 index 0000000000000000000000000000000000000000..aa1324e0c0862000e2fd0fbd54f5ad6e96a4844b GIT binary patch literal 833 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^2BvGCE{-7?&R0)fY+Py~a^%8E`p?ejxa7|0Q&O_gl%*+qY|w(fxh>*ZtGkQlPM8-tPqsXW($%^Y4}Y@n3e{ z_;h)8el;?>y>s&<$q6u6nICE1#W)iN&#T28Hm}v&mVPBS|1&bG{cRU#KNqG;@bQ}1fPot$DfyMXsT(2er~x9>fR zjK079UGcXBrYp_i`A&YW6Fe~ZqN;BG_aCnFyLIjN{@k`58O^oUhWmwa=J#o(izVP* z{h4#fwC3{jq-wXj-^}hKqwVh>f4z**H}|=nhJrB+E_nU3_TFQ0V4(c@^6d6KWK{NU y4J`P9^u6e7y-5hKZsxrH!FR+(+*4qJWjMaNbeiMV=v}~M&*16m=d#Wzp$PzC!lvI6;>1s;*b3=DjSL74G){)!Z!;8IT)$B+!?x05&W9x>oKvgiM$x+ikS*C?=Q zam;=ba%boBJNgClzEAys{l)r|ClyNk(7_Cz Lu6{1-oD!M!lvI6;>1s;*b3=DjSL74G){)!X^24*2o7srqc=hBXYoD7aUE!+MtdvDOM;FK`s z;P<^ZIkSvcoLykne2XW`IOE?u<|}CdvmJBUwn|<}yYR=B9WDoC{H*4K%K;gmzso@N zffYS_Z|MNh2Uc|UesKfHOsD~~_CIF;nF%$(r2af3TyE6j(V(Vd;;O#?o%stx>9i?n SfA0aa41=eupUXO@geCyCoQk*r literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-wave.png b/test/Common/res/images/brush/hatch/style-wave.png new file mode 100644 index 0000000000000000000000000000000000000000..90515d884ff68981cea4b6177ac0c06187910c88 GIT binary patch literal 658 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^1}1e+7srqc=eL&+u6<)A#&Yoa{>$YGHL9Wt zr!lvI6;>1s;*b3=DjSL74G){)!Z!hS#1hjv*P&SFc=L``AF>=!Gx$zeM{?+AJxU z=g?4@={2A2&#&se^Z(zut)2BkYFFF3h1oBp=G!fr{{72G*9AZ}MCdTn_iu+4r_X$A zv^h3y+vzi9GjH1c+;`>=-}~3c_luSA#yVfWU|PcaciVxzdlx|r0c!mCQn{+qUp}4% zYVNK!JDq}jdzqq{H}#Bfr>5;blLItz^83?g-hbFv_b1yCYzWA#eC|;PkrYDp>#yIp Z=Ts@3ma27TF)%GKc)I$ztaD0e0stkJE-wH8 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-wide-downward-diagonal.png b/test/Common/res/images/brush/hatch/style-wide-downward-diagonal.png new file mode 100644 index 0000000000000000000000000000000000000000..cdf87a71890be1d3c84029805c37011f724c5e8d GIT binary patch literal 754 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^2BvmT7srqc=eJjPPizg~IS}wG|CjD*_u@BF z94)H-!i%c^^gaH5-k6O|3I>nua5xNykEb5qA+D^?V*-Q!?63Fl`PkIf2!l)q!+4ot zur`7l&gNydl`KqNkO+fIln=m-J-p*sM=RWjHbcD&25?yMwA$Sa>KHDTXj7QYJYN!Z z;cOG0FDXZQ5hhIRV?bE^SV~z8<{LICH2=Jpy(y6ah8hAnR}t2>Z$+>TE=(3QN0{Jd z#e}f9C%Y#CY5>TX$4oH4^D_5_?<-G%`PgAYC?Xi;uN^}8-NE1O4w4BkIgl(~XP5*J vumy=d-0*PcF^T(mZz9acLoCpzJ^s$#y}oqX?wJ*!bjslA>gTe~DWM4fl%xBK literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-wide-upward-diagonal.png b/test/Common/res/images/brush/hatch/style-wide-upward-diagonal.png new file mode 100644 index 0000000000000000000000000000000000000000..9c2b1cb97f471618fb6827553661344d64d85e79 GIT binary patch literal 851 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^2BsIDE{-7?&Z(yu`<^QB9JsLa|I+_KUUS>S zR&~z(^8Wkw{r?Rvz~MbZSvDycJoe&^8H^32cPZRhmAC)~+m-V!!nFlN^h=xYz~Q{- zyWr{)7ku=*Zmfrp?D@-%Y=`x-!wzsLG578<1kXX7CE66O@k_xGUAWWOxLy}Fw>84x zvClE$aNBuI>|RzKYlTa;ZT!CgZiB<&isK~`8F09G^KCdgL*hkK!fKeAZH;n$J6sVO z6K(Sl;dG&5-s>iW%46!W=XbzEtXKc@Z9#I$s=%WdDyPuO-D1k#p={Gm;&TWiP{`2}t*x%S%Rz4(_J42ve&p9*M!7 z#@zaKhaMtPKu_xII9#JMe7jNbwcswPJ8$uDL^1I(z zgwVJ_&K%+444d<%$o5ZOmmY%@IluXl?I`zYN5t^QzWeXxTep@@yIY_o4a^7(p00i_ I>zopr0CTJ|2mk;8 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/hatch/style-zigzag.png b/test/Common/res/images/brush/hatch/style-zigzag.png new file mode 100644 index 0000000000000000000000000000000000000000..f4ce0f628c45fb335b64c04819d942e1ffa29449 GIT binary patch literal 890 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^24;Rw7srqc=hBmm>z*hGI3L{kf9e0Ow`Q7} zeIo7dAAkS;`~Q{DEbk1xg*#iG0U2jDZYW!R^X!|nGUMGd@8plRQX^&pxcy0`59 zT>lVeB}n%X8Sd=zSul&vF0+?kHXG)OzdhbCD?z&7zWWou1L5EdJ;URNZp{Td5ajI5 zFVDBXgt#8)3j4>C5Dt!U+HL(`{N1Q`M`rB4dH? Date: Sun, 4 Aug 2024 14:08:42 -0300 Subject: [PATCH 084/217] update color-green.png expected with #0F0 instead of #008000 --- .../res/images/brush/solid/color-green.png | Bin 228 -> 214 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/Common/res/images/brush/solid/color-green.png b/test/Common/res/images/brush/solid/color-green.png index 3be55607bd26777ac62b6345e68e9991dd905b8f..b1fcd524e89014c9f49d19475860d57c712ef0ac 100644 GIT binary patch delta 136 zcmaFDc#UyFSbesqi(^QJ^W9^Hf(!-%EDJy9|GK=6)yO30d*_vDw}PUxymMFTZe6;K dVvfkR>aU5V(mdKI;RB2CIEdGKp+4B delta 150 zcmcb{_=IsnSbe3Zi(^QJ^W77Mj0_44EDQVdigg_tixU#9cgH7)8FtTbJ)@K+xVa

Date: Sun, 4 Aug 2024 17:47:55 -0300 Subject: [PATCH 085/217] fix LinearGradientBrush.UpdateShader method --- src/Common/Drawing2D/LinearGradientBrush.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index cf34061..18891fe 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -467,7 +467,9 @@ private void UpdateShader(Action action) { action(); - var transform = new Matrix(m_transform.MatrixElements); + var points = new PointF[] { new(m_rect.Left, 0), new(m_rect.Right, 0) }; + + using var transform = new Matrix(m_transform.MatrixElements); switch (m_mode) { case WrapMode.TileFlipX: @@ -480,9 +482,11 @@ private void UpdateShader(Action action) transform.Scale(-1, -1); break; } + transform.TransformPoints(points); + transform.Reset(); - var start = new SKPoint(0, m_rect.Left); - var end = new SKPoint(0, m_rect.Right); + var start = points[0].m_point; + var end = points[1].m_point; var matrix = transform.m_matrix; var gamma = m_gamma ? 2.2f : 1.0f; var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; From 17f7511834c974b6213864f708f4453cb75218d2 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Sun, 4 Aug 2024 18:24:00 -0300 Subject: [PATCH 086/217] simplify test cases comparing images and rename expected images with exactly the same name as the value they are testing --- test/Common/Drawing2D/HatchBrushUnitTest.cs | 116 +++++++++--------- .../Drawing2D/LinearGradientBrushUnitTest.cs | 24 ++-- test/Common/SolidBrushUnitTest.cs | 12 +- test/Common/TextureBrushUnitTest.cs | 8 +- ...diagonal.png => StyleBackwardDiagonal.png} | Bin .../hatch/{style-cross.png => StyleCross.png} | Bin ...onal.png => StyleDarkDownwardDiagonal.png} | Bin ...horizontal.png => StyleDarkHorizontal.png} | Bin ...agonal.png => StyleDarkUpwardDiagonal.png} | Bin ...ark-vertical.png => StyleDarkVertical.png} | Bin ...al.png => StyleDashedDownwardDiagonal.png} | Bin ...rizontal.png => StyleDashedHorizontal.png} | Bin ...onal.png => StyleDashedUpwardDiagonal.png} | Bin ...d-vertical.png => StyleDashedVertical.png} | Bin ...gonal-brick.png => StyleDiagonalBrick.png} | Bin ...gonal-cross.png => StyleDiagonalCross.png} | Bin .../hatch/{style-divot.png => StyleDivot.png} | Bin ...ted-diamond.png => StyleDottedDiamond.png} | Bin ...le-dotted-grid.png => StyleDottedGrid.png} | Bin ...-diagonal.png => StyleForwardDiagonal.png} | Bin ...yle-horizontal.png => StyleHorizontal.png} | Bin ...tal-brick.png => StyleHorizontalBrick.png} | Bin ...r-board.png => StyleLargeCheckerBoard.png} | Bin ...ge-confetti.png => StyleLargeConfetti.png} | Bin ...nal.png => StyleLightDownwardDiagonal.png} | Bin ...orizontal.png => StyleLightHorizontal.png} | Bin ...gonal.png => StyleLightUpwardDiagonal.png} | Bin ...ht-vertical.png => StyleLightVertical.png} | Bin ...rizontal.png => StyleNarrowHorizontal.png} | Bin ...w-vertical.png => StyleNarrowVertical.png} | Bin ...d-diamond.png => StyleOutlinedDiamond.png} | Bin ...style-percent05.png => StylePercent05.png} | Bin ...style-percent10.png => StylePercent10.png} | Bin ...style-percent20.png => StylePercent20.png} | Bin ...style-percent25.png => StylePercent25.png} | Bin ...style-percent30.png => StylePercent30.png} | Bin ...style-percent40.png => StylePercent40.png} | Bin ...style-percent50.png => StylePercent50.png} | Bin ...style-percent60.png => StylePercent60.png} | Bin ...style-percent70.png => StylePercent70.png} | Bin ...style-percent75.png => StylePercent75.png} | Bin ...style-percent80.png => StylePercent80.png} | Bin ...style-percent90.png => StylePercent90.png} | Bin .../hatch/{style-plaid.png => StylePlaid.png} | Bin .../{style-shingle.png => StyleShingle.png} | Bin ...r-board.png => StyleSmallCheckerBoard.png} | Bin ...ll-confetti.png => StyleSmallConfetti.png} | Bin ...tyle-small-grid.png => StyleSmallGrid.png} | Bin ...olid-diamond.png => StyleSolidDiamond.png} | Bin .../{style-sphere.png => StyleSphere.png} | Bin .../{style-trellis.png => StyleTrellis.png} | Bin .../{style-vertical.png => StyleVertical.png} | Bin .../hatch/{style-wave.png => StyleWave.png} | Bin .../hatch/{style-weave.png => StyleWeave.png} | Bin ...onal.png => StyleWideDownwardDiagonal.png} | Bin ...agonal.png => StyleWideUpwardDiagonal.png} | Bin .../{style-zigzag.png => StyleZigzag.png} | Bin .../brush/linear/{angle-0.png => Angle0.png} | Bin .../linear/{angle-45.png => Angle45.png} | Bin .../linear/{angle-75.png => Angle75.png} | Bin .../{mode-bd.png => ModeBackwardDiagonal.png} | Bin .../{mode-fd.png => ModeForwardDiagonal.png} | Bin .../linear/{mode-h.png => ModeHorizontal.png} | Bin .../linear/{angle-90.png => ModeVertical.png} | Bin .../brush/linear/{mode-v.png => angle90.png} | Bin .../solid/{color-blue.png => ColorBlue.png} | Bin .../res/images/brush/solid/ColorGreen.png | Bin 0 -> 228 bytes .../solid/{color-red.png => ColorRed.png} | Bin .../res/images/brush/solid/color-green.png | Bin 214 -> 0 bytes .../{wrap-clamp.png => ModeClamp.png} | Bin .../textured/{wrap-tile.png => ModeTile.png} | Bin 71 files changed, 83 insertions(+), 77 deletions(-) rename test/Common/res/images/brush/hatch/{style-backward-diagonal.png => StyleBackwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-cross.png => StyleCross.png} (100%) rename test/Common/res/images/brush/hatch/{style-dark-downward-diagonal.png => StyleDarkDownwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-dark-horizontal.png => StyleDarkHorizontal.png} (100%) rename test/Common/res/images/brush/hatch/{style-dark-upward-diagonal.png => StyleDarkUpwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-dark-vertical.png => StyleDarkVertical.png} (100%) rename test/Common/res/images/brush/hatch/{style-dashed-downward-diagonal.png => StyleDashedDownwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-dashed-horizontal.png => StyleDashedHorizontal.png} (100%) rename test/Common/res/images/brush/hatch/{style-dashed-upward-diagonal.png => StyleDashedUpwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-dashed-vertical.png => StyleDashedVertical.png} (100%) rename test/Common/res/images/brush/hatch/{style-diagonal-brick.png => StyleDiagonalBrick.png} (100%) rename test/Common/res/images/brush/hatch/{style-diagonal-cross.png => StyleDiagonalCross.png} (100%) rename test/Common/res/images/brush/hatch/{style-divot.png => StyleDivot.png} (100%) rename test/Common/res/images/brush/hatch/{style-dotted-diamond.png => StyleDottedDiamond.png} (100%) rename test/Common/res/images/brush/hatch/{style-dotted-grid.png => StyleDottedGrid.png} (100%) rename test/Common/res/images/brush/hatch/{style-forward-diagonal.png => StyleForwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-horizontal.png => StyleHorizontal.png} (100%) rename test/Common/res/images/brush/hatch/{style-horizontal-brick.png => StyleHorizontalBrick.png} (100%) rename test/Common/res/images/brush/hatch/{style-large-checker-board.png => StyleLargeCheckerBoard.png} (100%) rename test/Common/res/images/brush/hatch/{style-large-confetti.png => StyleLargeConfetti.png} (100%) rename test/Common/res/images/brush/hatch/{style-light-downward-diagonal.png => StyleLightDownwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-light-horizontal.png => StyleLightHorizontal.png} (100%) rename test/Common/res/images/brush/hatch/{style-light-upward-diagonal.png => StyleLightUpwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-light-vertical.png => StyleLightVertical.png} (100%) rename test/Common/res/images/brush/hatch/{style-narrow-horizontal.png => StyleNarrowHorizontal.png} (100%) rename test/Common/res/images/brush/hatch/{style-narrow-vertical.png => StyleNarrowVertical.png} (100%) rename test/Common/res/images/brush/hatch/{style-outlined-diamond.png => StyleOutlinedDiamond.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent05.png => StylePercent05.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent10.png => StylePercent10.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent20.png => StylePercent20.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent25.png => StylePercent25.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent30.png => StylePercent30.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent40.png => StylePercent40.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent50.png => StylePercent50.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent60.png => StylePercent60.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent70.png => StylePercent70.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent75.png => StylePercent75.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent80.png => StylePercent80.png} (100%) rename test/Common/res/images/brush/hatch/{style-percent90.png => StylePercent90.png} (100%) rename test/Common/res/images/brush/hatch/{style-plaid.png => StylePlaid.png} (100%) rename test/Common/res/images/brush/hatch/{style-shingle.png => StyleShingle.png} (100%) rename test/Common/res/images/brush/hatch/{style-small-checker-board.png => StyleSmallCheckerBoard.png} (100%) rename test/Common/res/images/brush/hatch/{style-small-confetti.png => StyleSmallConfetti.png} (100%) rename test/Common/res/images/brush/hatch/{style-small-grid.png => StyleSmallGrid.png} (100%) rename test/Common/res/images/brush/hatch/{style-solid-diamond.png => StyleSolidDiamond.png} (100%) rename test/Common/res/images/brush/hatch/{style-sphere.png => StyleSphere.png} (100%) rename test/Common/res/images/brush/hatch/{style-trellis.png => StyleTrellis.png} (100%) rename test/Common/res/images/brush/hatch/{style-vertical.png => StyleVertical.png} (100%) rename test/Common/res/images/brush/hatch/{style-wave.png => StyleWave.png} (100%) rename test/Common/res/images/brush/hatch/{style-weave.png => StyleWeave.png} (100%) rename test/Common/res/images/brush/hatch/{style-wide-downward-diagonal.png => StyleWideDownwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-wide-upward-diagonal.png => StyleWideUpwardDiagonal.png} (100%) rename test/Common/res/images/brush/hatch/{style-zigzag.png => StyleZigzag.png} (100%) rename test/Common/res/images/brush/linear/{angle-0.png => Angle0.png} (100%) rename test/Common/res/images/brush/linear/{angle-45.png => Angle45.png} (100%) rename test/Common/res/images/brush/linear/{angle-75.png => Angle75.png} (100%) rename test/Common/res/images/brush/linear/{mode-bd.png => ModeBackwardDiagonal.png} (100%) rename test/Common/res/images/brush/linear/{mode-fd.png => ModeForwardDiagonal.png} (100%) rename test/Common/res/images/brush/linear/{mode-h.png => ModeHorizontal.png} (100%) rename test/Common/res/images/brush/linear/{angle-90.png => ModeVertical.png} (100%) rename test/Common/res/images/brush/linear/{mode-v.png => angle90.png} (100%) rename test/Common/res/images/brush/solid/{color-blue.png => ColorBlue.png} (100%) create mode 100644 test/Common/res/images/brush/solid/ColorGreen.png rename test/Common/res/images/brush/solid/{color-red.png => ColorRed.png} (100%) delete mode 100644 test/Common/res/images/brush/solid/color-green.png rename test/Common/res/images/brush/textured/{wrap-clamp.png => ModeClamp.png} (100%) rename test/Common/res/images/brush/textured/{wrap-tile.png => ModeTile.png} (100%) diff --git a/test/Common/Drawing2D/HatchBrushUnitTest.cs b/test/Common/Drawing2D/HatchBrushUnitTest.cs index dc08e27..5c95bb1 100644 --- a/test/Common/Drawing2D/HatchBrushUnitTest.cs +++ b/test/Common/Drawing2D/HatchBrushUnitTest.cs @@ -11,60 +11,60 @@ public void Setup() } [Test] - [TestCase(HatchStyle.Horizontal, "style-horizontal.png")] - [TestCase(HatchStyle.Vertical, "style-vertical.png")] - [TestCase(HatchStyle.ForwardDiagonal, "style-forward-diagonal.png")] - [TestCase(HatchStyle.BackwardDiagonal, "style-backward-diagonal.png")] - [TestCase(HatchStyle.Cross, "style-cross.png")] - [TestCase(HatchStyle.DiagonalCross, "style-diagonal-cross.png")] - [TestCase(HatchStyle.Percent05, "style-percent05.png")] - [TestCase(HatchStyle.Percent10, "style-percent10.png")] - [TestCase(HatchStyle.Percent20, "style-percent20.png")] - [TestCase(HatchStyle.Percent25, "style-percent25.png")] - [TestCase(HatchStyle.Percent30, "style-percent30.png")] - [TestCase(HatchStyle.Percent40, "style-percent40.png")] - [TestCase(HatchStyle.Percent50, "style-percent50.png")] - [TestCase(HatchStyle.Percent60, "style-percent60.png")] - [TestCase(HatchStyle.Percent70, "style-percent70.png")] - [TestCase(HatchStyle.Percent75, "style-percent75.png")] - [TestCase(HatchStyle.Percent80, "style-percent80.png")] - [TestCase(HatchStyle.Percent90, "style-percent90.png")] - [TestCase(HatchStyle.LightDownwardDiagonal, "style-light-downward-diagonal.png")] - [TestCase(HatchStyle.LightUpwardDiagonal, "style-light-upward-diagonal.png")] - [TestCase(HatchStyle.DarkDownwardDiagonal, "style-dark-downward-diagonal.png")] - [TestCase(HatchStyle.DarkUpwardDiagonal, "style-dark-upward-diagonal.png")] - [TestCase(HatchStyle.WideDownwardDiagonal, "style-wide-downward-diagonal.png")] - [TestCase(HatchStyle.WideUpwardDiagonal, "style-wide-upward-diagonal.png")] - [TestCase(HatchStyle.LightVertical, "style-light-vertical.png")] - [TestCase(HatchStyle.LightHorizontal, "style-light-horizontal.png")] - [TestCase(HatchStyle.NarrowVertical, "style-narrow-vertical.png")] - [TestCase(HatchStyle.NarrowHorizontal, "style-narrow-horizontal.png")] - [TestCase(HatchStyle.DarkVertical, "style-dark-vertical.png")] - [TestCase(HatchStyle.DarkHorizontal, "style-dark-horizontal.png")] - [TestCase(HatchStyle.DashedDownwardDiagonal, "style-dashed-downward-diagonal.png")] - [TestCase(HatchStyle.DashedUpwardDiagonal, "style-dashed-upward-diagonal.png")] - [TestCase(HatchStyle.DashedHorizontal, "style-dashed-horizontal.png")] - [TestCase(HatchStyle.DashedVertical, "style-dashed-vertical.png")] - [TestCase(HatchStyle.SmallConfetti, "style-small-confetti.png")] - [TestCase(HatchStyle.LargeConfetti, "style-large-confetti.png")] - [TestCase(HatchStyle.ZigZag, "style-zigzag.png")] - [TestCase(HatchStyle.Wave, "style-wave.png")] - [TestCase(HatchStyle.DiagonalBrick, "style-diagonal-brick.png")] - [TestCase(HatchStyle.HorizontalBrick, "style-horizontal-brick.png")] - [TestCase(HatchStyle.Weave, "style-weave.png")] - [TestCase(HatchStyle.Plaid, "style-plaid.png")] - [TestCase(HatchStyle.Divot, "style-divot.png")] - [TestCase(HatchStyle.DottedGrid, "style-dotted-grid.png")] - [TestCase(HatchStyle.DottedDiamond, "style-dotted-diamond.png")] - [TestCase(HatchStyle.Shingle, "style-shingle.png")] - [TestCase(HatchStyle.Trellis, "style-trellis.png")] - [TestCase(HatchStyle.Sphere, "style-sphere.png")] - [TestCase(HatchStyle.SmallGrid, "style-small-grid.png")] - [TestCase(HatchStyle.SmallCheckerBoard, "style-small-checker-board.png")] - [TestCase(HatchStyle.LargeCheckerBoard, "style-large-checker-board.png")] - [TestCase(HatchStyle.OutlinedDiamond, "style-outlined-diamond.png")] - [TestCase(HatchStyle.SolidDiamond, "style-solid-diamond.png")] - public void Constructor_StyleForeBack(HatchStyle style, string expected) + [TestCase(HatchStyle.Horizontal)] + [TestCase(HatchStyle.Vertical)] + [TestCase(HatchStyle.ForwardDiagonal)] + [TestCase(HatchStyle.BackwardDiagonal)] + [TestCase(HatchStyle.Cross)] + [TestCase(HatchStyle.DiagonalCross)] + [TestCase(HatchStyle.Percent05)] + [TestCase(HatchStyle.Percent10)] + [TestCase(HatchStyle.Percent20)] + [TestCase(HatchStyle.Percent25)] + [TestCase(HatchStyle.Percent30)] + [TestCase(HatchStyle.Percent40)] + [TestCase(HatchStyle.Percent50)] + [TestCase(HatchStyle.Percent60)] + [TestCase(HatchStyle.Percent70)] + [TestCase(HatchStyle.Percent75)] + [TestCase(HatchStyle.Percent80)] + [TestCase(HatchStyle.Percent90)] + [TestCase(HatchStyle.LightDownwardDiagonal)] + [TestCase(HatchStyle.LightUpwardDiagonal)] + [TestCase(HatchStyle.DarkDownwardDiagonal)] + [TestCase(HatchStyle.DarkUpwardDiagonal)] + [TestCase(HatchStyle.WideDownwardDiagonal)] + [TestCase(HatchStyle.WideUpwardDiagonal)] + [TestCase(HatchStyle.LightVertical)] + [TestCase(HatchStyle.LightHorizontal)] + [TestCase(HatchStyle.NarrowVertical)] + [TestCase(HatchStyle.NarrowHorizontal)] + [TestCase(HatchStyle.DarkVertical)] + [TestCase(HatchStyle.DarkHorizontal)] + [TestCase(HatchStyle.DashedDownwardDiagonal)] + [TestCase(HatchStyle.DashedUpwardDiagonal)] + [TestCase(HatchStyle.DashedHorizontal)] + [TestCase(HatchStyle.DashedVertical)] + [TestCase(HatchStyle.SmallConfetti)] + [TestCase(HatchStyle.LargeConfetti)] + [TestCase(HatchStyle.ZigZag)] + [TestCase(HatchStyle.Wave)] + [TestCase(HatchStyle.DiagonalBrick)] + [TestCase(HatchStyle.HorizontalBrick)] + [TestCase(HatchStyle.Weave)] + [TestCase(HatchStyle.Plaid)] + [TestCase(HatchStyle.Divot)] + [TestCase(HatchStyle.DottedGrid)] + [TestCase(HatchStyle.DottedDiamond)] + [TestCase(HatchStyle.Shingle)] + [TestCase(HatchStyle.Trellis)] + [TestCase(HatchStyle.Sphere)] + [TestCase(HatchStyle.SmallGrid)] + [TestCase(HatchStyle.SmallCheckerBoard)] + [TestCase(HatchStyle.LargeCheckerBoard)] + [TestCase(HatchStyle.OutlinedDiamond)] + [TestCase(HatchStyle.SolidDiamond)] + public void Constructor_StyleForeBack(HatchStyle style) { var back = Color.Red; var fore = Color.Blue; @@ -76,7 +76,13 @@ public void Constructor_StyleForeBack(HatchStyle style, string expected) Assert.That(brush.ForegroundColor, Is.EqualTo(fore)); Assert.That(brush.HatchStyle, Is.EqualTo(style)); - string path = Path.Combine("brush", "hatch", expected); + string name = style switch + { + HatchStyle.Cross => "Cross", // defines LargeGrid nad Max aliases + HatchStyle.Horizontal => "Horizontal", // defines Min alias + _ => style.ToString() + }; + string path = Path.Combine("brush", "hatch", $"Style{name}.png"); float similarity = Utils.CompareImage(path, brush, true); Assert.That(similarity, Is.GreaterThan(0.95)); }); diff --git a/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs index 552b81c..e45f918 100644 --- a/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs +++ b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs @@ -32,11 +32,11 @@ public void Constructor_Points() } [Test] - [TestCase(LinearGradientMode.Horizontal, "mode-h.png", new[] { 0.9999999f, 0f, 0f, 0.9999999f, 0f, 0f })] - [TestCase(LinearGradientMode.Vertical, "mode-v.png", new[] { -1.907349E-07f, 0.9999999f, -1f, -4.768372E-08f, 50f, 7.152557E-07f })] - [TestCase(LinearGradientMode.BackwardDiagonal, "mode-bd.png", new[] { -1f, 0.9999998f, -1f, -1f, 74.99999f, 25f })] - [TestCase(LinearGradientMode.ForwardDiagonal, "mode-fd.png", new[] { 0.9999999f, 0.9999999f, -1f, 0.9999999f, 25f, -25f })] - public void Constructor_RectangleMode(LinearGradientMode mode, string expected, float[] elements) + [TestCase(LinearGradientMode.Horizontal, new[] { 0.9999999f, 0f, 0f, 0.9999999f, 0f, 0f })] + [TestCase(LinearGradientMode.Vertical, new[] { -1.907349E-07f, 0.9999999f, -1f, -4.768372E-08f, 50f, 7.152557E-07f })] + [TestCase(LinearGradientMode.BackwardDiagonal, new[] { -1f, 0.9999998f, -1f, -1f, 74.99999f, 25f })] + [TestCase(LinearGradientMode.ForwardDiagonal, new[] { 0.9999999f, 0.9999999f, -1f, 0.9999999f, 25f, -25f })] + public void Constructor_RectangleMode(LinearGradientMode mode, float[] elements) { var rect = new RectangleF(15, 15, 20, 20); @@ -51,18 +51,18 @@ public void Constructor_RectangleMode(LinearGradientMode mode, string expected, Assert.That(brush.Rectangle, Is.EqualTo(rect)); Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile)); - string path = Path.Combine("brush", "linear", expected); + string path = Path.Combine("brush", "linear", $"Mode{mode}.png"); float similarity = Utils.CompareImage(path, brush, true); Assert.That(similarity, Is.GreaterThan(0.95)); }); } [Test] - [TestCase(0f, "angle-0.png", new[] { 0.9999999f, 0f, 0f, 0.9999999f, 0f, 0f })] - [TestCase(45f, "angle-45.png", new[] { 0.999999f, 0.9999999f, -1f, 0.9999999f, 25f, -25f })] - [TestCase(75f, "angle-75.png", new[] { 0.3169872f, 1.183012f, -1.183013f, 0.3169872f, 46.65063f, -12.5f })] - [TestCase(90f, "angle-90.png", new[] { -1.907349E-07f, 0.9999999f, -1f, -4.768372E-08f, 50f, 7.152557E-07f })] - public void Constructor_RectangleAngle(float angle, string expected, float[] elements) + [TestCase(0f, new[] { 0.9999999f, 0f, 0f, 0.9999999f, 0f, 0f })] + [TestCase(45f, new[] { 0.999999f, 0.9999999f, -1f, 0.9999999f, 25f, -25f })] + [TestCase(75f, new[] { 0.3169872f, 1.183012f, -1.183013f, 0.3169872f, 46.65063f, -12.5f })] + [TestCase(90f, new[] { -1.907349E-07f, 0.9999999f, -1f, -4.768372E-08f, 50f, 7.152557E-07f })] + public void Constructor_RectangleAngle(float angle, float[] elements) { var rect = new RectangleF(15, 15, 20, 20); @@ -77,7 +77,7 @@ public void Constructor_RectangleAngle(float angle, string expected, float[] ele Assert.That(brush.Rectangle, Is.EqualTo(rect)); Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile)); - string path = Path.Combine("brush", "linear", expected); + string path = Path.Combine("brush", "linear", $"Angle{angle}.png"); float similarity = Utils.CompareImage(path, brush, true); Assert.That(similarity, Is.GreaterThan(0.95)); }); diff --git a/test/Common/SolidBrushUnitTest.cs b/test/Common/SolidBrushUnitTest.cs index 8f52ffd..b81aea7 100644 --- a/test/Common/SolidBrushUnitTest.cs +++ b/test/Common/SolidBrushUnitTest.cs @@ -10,19 +10,19 @@ public void Setup() } [Test] - [TestCase("#FF0000", "color-red.png")] - [TestCase("#00FF00", "color-green.png")] - [TestCase("#0000FF", "color-blue.png")] - public void Constructor_Color(string hex, string expected) + [TestCase("Red")] + [TestCase("Green")] + [TestCase("Blue")] + public void Constructor_Color(string name) { - var color = new Color(hex); + var color = Color.FromName(name); using var brush = new SolidBrush(color); Assert.Multiple(() => { Assert.That(brush.Color, Is.EqualTo(color)); - string path = Path.Combine("brush", "solid", expected); + string path = Path.Combine("brush", "solid", $"Color{name}.png"); float similarity = Utils.CompareImage(path, brush, true); Assert.That(similarity, Is.GreaterThan(0.95)); }); diff --git a/test/Common/TextureBrushUnitTest.cs b/test/Common/TextureBrushUnitTest.cs index 7204ddc..9ab19ef 100644 --- a/test/Common/TextureBrushUnitTest.cs +++ b/test/Common/TextureBrushUnitTest.cs @@ -16,9 +16,9 @@ public void Setup() } [Test] - [TestCase(WrapMode.Tile, "wrap-tile.png")] - [TestCase(WrapMode.Clamp, "wrap-clamp.png")] - public void Constructor_BitmapRectWrap(WrapMode mode, string expected) + [TestCase(WrapMode.Tile)] + [TestCase(WrapMode.Clamp)] + public void Constructor_BitmapRectWrap(WrapMode mode) { var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); using var bitmap = new Bitmap(filePath); @@ -35,7 +35,7 @@ public void Constructor_BitmapRectWrap(WrapMode mode, string expected) Assert.That(brush.WrapMode, Is.EqualTo(mode)); Assert.That(brush.Transform, Is.EqualTo(matrix)); - string path = Path.Combine("brush", "textured", expected); + string path = Path.Combine("brush", "textured", $"Mode{mode}.png"); float similarity = Utils.CompareImage(path, brush, true); Assert.That(similarity, Is.GreaterThan(0.95)); }); diff --git a/test/Common/res/images/brush/hatch/style-backward-diagonal.png b/test/Common/res/images/brush/hatch/StyleBackwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-backward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleBackwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-cross.png b/test/Common/res/images/brush/hatch/StyleCross.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-cross.png rename to test/Common/res/images/brush/hatch/StyleCross.png diff --git a/test/Common/res/images/brush/hatch/style-dark-downward-diagonal.png b/test/Common/res/images/brush/hatch/StyleDarkDownwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dark-downward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleDarkDownwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-dark-horizontal.png b/test/Common/res/images/brush/hatch/StyleDarkHorizontal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dark-horizontal.png rename to test/Common/res/images/brush/hatch/StyleDarkHorizontal.png diff --git a/test/Common/res/images/brush/hatch/style-dark-upward-diagonal.png b/test/Common/res/images/brush/hatch/StyleDarkUpwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dark-upward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleDarkUpwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-dark-vertical.png b/test/Common/res/images/brush/hatch/StyleDarkVertical.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dark-vertical.png rename to test/Common/res/images/brush/hatch/StyleDarkVertical.png diff --git a/test/Common/res/images/brush/hatch/style-dashed-downward-diagonal.png b/test/Common/res/images/brush/hatch/StyleDashedDownwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dashed-downward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleDashedDownwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-dashed-horizontal.png b/test/Common/res/images/brush/hatch/StyleDashedHorizontal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dashed-horizontal.png rename to test/Common/res/images/brush/hatch/StyleDashedHorizontal.png diff --git a/test/Common/res/images/brush/hatch/style-dashed-upward-diagonal.png b/test/Common/res/images/brush/hatch/StyleDashedUpwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dashed-upward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleDashedUpwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-dashed-vertical.png b/test/Common/res/images/brush/hatch/StyleDashedVertical.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dashed-vertical.png rename to test/Common/res/images/brush/hatch/StyleDashedVertical.png diff --git a/test/Common/res/images/brush/hatch/style-diagonal-brick.png b/test/Common/res/images/brush/hatch/StyleDiagonalBrick.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-diagonal-brick.png rename to test/Common/res/images/brush/hatch/StyleDiagonalBrick.png diff --git a/test/Common/res/images/brush/hatch/style-diagonal-cross.png b/test/Common/res/images/brush/hatch/StyleDiagonalCross.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-diagonal-cross.png rename to test/Common/res/images/brush/hatch/StyleDiagonalCross.png diff --git a/test/Common/res/images/brush/hatch/style-divot.png b/test/Common/res/images/brush/hatch/StyleDivot.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-divot.png rename to test/Common/res/images/brush/hatch/StyleDivot.png diff --git a/test/Common/res/images/brush/hatch/style-dotted-diamond.png b/test/Common/res/images/brush/hatch/StyleDottedDiamond.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dotted-diamond.png rename to test/Common/res/images/brush/hatch/StyleDottedDiamond.png diff --git a/test/Common/res/images/brush/hatch/style-dotted-grid.png b/test/Common/res/images/brush/hatch/StyleDottedGrid.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-dotted-grid.png rename to test/Common/res/images/brush/hatch/StyleDottedGrid.png diff --git a/test/Common/res/images/brush/hatch/style-forward-diagonal.png b/test/Common/res/images/brush/hatch/StyleForwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-forward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleForwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-horizontal.png b/test/Common/res/images/brush/hatch/StyleHorizontal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-horizontal.png rename to test/Common/res/images/brush/hatch/StyleHorizontal.png diff --git a/test/Common/res/images/brush/hatch/style-horizontal-brick.png b/test/Common/res/images/brush/hatch/StyleHorizontalBrick.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-horizontal-brick.png rename to test/Common/res/images/brush/hatch/StyleHorizontalBrick.png diff --git a/test/Common/res/images/brush/hatch/style-large-checker-board.png b/test/Common/res/images/brush/hatch/StyleLargeCheckerBoard.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-large-checker-board.png rename to test/Common/res/images/brush/hatch/StyleLargeCheckerBoard.png diff --git a/test/Common/res/images/brush/hatch/style-large-confetti.png b/test/Common/res/images/brush/hatch/StyleLargeConfetti.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-large-confetti.png rename to test/Common/res/images/brush/hatch/StyleLargeConfetti.png diff --git a/test/Common/res/images/brush/hatch/style-light-downward-diagonal.png b/test/Common/res/images/brush/hatch/StyleLightDownwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-light-downward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleLightDownwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-light-horizontal.png b/test/Common/res/images/brush/hatch/StyleLightHorizontal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-light-horizontal.png rename to test/Common/res/images/brush/hatch/StyleLightHorizontal.png diff --git a/test/Common/res/images/brush/hatch/style-light-upward-diagonal.png b/test/Common/res/images/brush/hatch/StyleLightUpwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-light-upward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleLightUpwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-light-vertical.png b/test/Common/res/images/brush/hatch/StyleLightVertical.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-light-vertical.png rename to test/Common/res/images/brush/hatch/StyleLightVertical.png diff --git a/test/Common/res/images/brush/hatch/style-narrow-horizontal.png b/test/Common/res/images/brush/hatch/StyleNarrowHorizontal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-narrow-horizontal.png rename to test/Common/res/images/brush/hatch/StyleNarrowHorizontal.png diff --git a/test/Common/res/images/brush/hatch/style-narrow-vertical.png b/test/Common/res/images/brush/hatch/StyleNarrowVertical.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-narrow-vertical.png rename to test/Common/res/images/brush/hatch/StyleNarrowVertical.png diff --git a/test/Common/res/images/brush/hatch/style-outlined-diamond.png b/test/Common/res/images/brush/hatch/StyleOutlinedDiamond.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-outlined-diamond.png rename to test/Common/res/images/brush/hatch/StyleOutlinedDiamond.png diff --git a/test/Common/res/images/brush/hatch/style-percent05.png b/test/Common/res/images/brush/hatch/StylePercent05.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent05.png rename to test/Common/res/images/brush/hatch/StylePercent05.png diff --git a/test/Common/res/images/brush/hatch/style-percent10.png b/test/Common/res/images/brush/hatch/StylePercent10.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent10.png rename to test/Common/res/images/brush/hatch/StylePercent10.png diff --git a/test/Common/res/images/brush/hatch/style-percent20.png b/test/Common/res/images/brush/hatch/StylePercent20.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent20.png rename to test/Common/res/images/brush/hatch/StylePercent20.png diff --git a/test/Common/res/images/brush/hatch/style-percent25.png b/test/Common/res/images/brush/hatch/StylePercent25.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent25.png rename to test/Common/res/images/brush/hatch/StylePercent25.png diff --git a/test/Common/res/images/brush/hatch/style-percent30.png b/test/Common/res/images/brush/hatch/StylePercent30.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent30.png rename to test/Common/res/images/brush/hatch/StylePercent30.png diff --git a/test/Common/res/images/brush/hatch/style-percent40.png b/test/Common/res/images/brush/hatch/StylePercent40.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent40.png rename to test/Common/res/images/brush/hatch/StylePercent40.png diff --git a/test/Common/res/images/brush/hatch/style-percent50.png b/test/Common/res/images/brush/hatch/StylePercent50.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent50.png rename to test/Common/res/images/brush/hatch/StylePercent50.png diff --git a/test/Common/res/images/brush/hatch/style-percent60.png b/test/Common/res/images/brush/hatch/StylePercent60.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent60.png rename to test/Common/res/images/brush/hatch/StylePercent60.png diff --git a/test/Common/res/images/brush/hatch/style-percent70.png b/test/Common/res/images/brush/hatch/StylePercent70.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent70.png rename to test/Common/res/images/brush/hatch/StylePercent70.png diff --git a/test/Common/res/images/brush/hatch/style-percent75.png b/test/Common/res/images/brush/hatch/StylePercent75.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent75.png rename to test/Common/res/images/brush/hatch/StylePercent75.png diff --git a/test/Common/res/images/brush/hatch/style-percent80.png b/test/Common/res/images/brush/hatch/StylePercent80.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent80.png rename to test/Common/res/images/brush/hatch/StylePercent80.png diff --git a/test/Common/res/images/brush/hatch/style-percent90.png b/test/Common/res/images/brush/hatch/StylePercent90.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-percent90.png rename to test/Common/res/images/brush/hatch/StylePercent90.png diff --git a/test/Common/res/images/brush/hatch/style-plaid.png b/test/Common/res/images/brush/hatch/StylePlaid.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-plaid.png rename to test/Common/res/images/brush/hatch/StylePlaid.png diff --git a/test/Common/res/images/brush/hatch/style-shingle.png b/test/Common/res/images/brush/hatch/StyleShingle.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-shingle.png rename to test/Common/res/images/brush/hatch/StyleShingle.png diff --git a/test/Common/res/images/brush/hatch/style-small-checker-board.png b/test/Common/res/images/brush/hatch/StyleSmallCheckerBoard.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-small-checker-board.png rename to test/Common/res/images/brush/hatch/StyleSmallCheckerBoard.png diff --git a/test/Common/res/images/brush/hatch/style-small-confetti.png b/test/Common/res/images/brush/hatch/StyleSmallConfetti.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-small-confetti.png rename to test/Common/res/images/brush/hatch/StyleSmallConfetti.png diff --git a/test/Common/res/images/brush/hatch/style-small-grid.png b/test/Common/res/images/brush/hatch/StyleSmallGrid.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-small-grid.png rename to test/Common/res/images/brush/hatch/StyleSmallGrid.png diff --git a/test/Common/res/images/brush/hatch/style-solid-diamond.png b/test/Common/res/images/brush/hatch/StyleSolidDiamond.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-solid-diamond.png rename to test/Common/res/images/brush/hatch/StyleSolidDiamond.png diff --git a/test/Common/res/images/brush/hatch/style-sphere.png b/test/Common/res/images/brush/hatch/StyleSphere.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-sphere.png rename to test/Common/res/images/brush/hatch/StyleSphere.png diff --git a/test/Common/res/images/brush/hatch/style-trellis.png b/test/Common/res/images/brush/hatch/StyleTrellis.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-trellis.png rename to test/Common/res/images/brush/hatch/StyleTrellis.png diff --git a/test/Common/res/images/brush/hatch/style-vertical.png b/test/Common/res/images/brush/hatch/StyleVertical.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-vertical.png rename to test/Common/res/images/brush/hatch/StyleVertical.png diff --git a/test/Common/res/images/brush/hatch/style-wave.png b/test/Common/res/images/brush/hatch/StyleWave.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-wave.png rename to test/Common/res/images/brush/hatch/StyleWave.png diff --git a/test/Common/res/images/brush/hatch/style-weave.png b/test/Common/res/images/brush/hatch/StyleWeave.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-weave.png rename to test/Common/res/images/brush/hatch/StyleWeave.png diff --git a/test/Common/res/images/brush/hatch/style-wide-downward-diagonal.png b/test/Common/res/images/brush/hatch/StyleWideDownwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-wide-downward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleWideDownwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-wide-upward-diagonal.png b/test/Common/res/images/brush/hatch/StyleWideUpwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-wide-upward-diagonal.png rename to test/Common/res/images/brush/hatch/StyleWideUpwardDiagonal.png diff --git a/test/Common/res/images/brush/hatch/style-zigzag.png b/test/Common/res/images/brush/hatch/StyleZigzag.png similarity index 100% rename from test/Common/res/images/brush/hatch/style-zigzag.png rename to test/Common/res/images/brush/hatch/StyleZigzag.png diff --git a/test/Common/res/images/brush/linear/angle-0.png b/test/Common/res/images/brush/linear/Angle0.png similarity index 100% rename from test/Common/res/images/brush/linear/angle-0.png rename to test/Common/res/images/brush/linear/Angle0.png diff --git a/test/Common/res/images/brush/linear/angle-45.png b/test/Common/res/images/brush/linear/Angle45.png similarity index 100% rename from test/Common/res/images/brush/linear/angle-45.png rename to test/Common/res/images/brush/linear/Angle45.png diff --git a/test/Common/res/images/brush/linear/angle-75.png b/test/Common/res/images/brush/linear/Angle75.png similarity index 100% rename from test/Common/res/images/brush/linear/angle-75.png rename to test/Common/res/images/brush/linear/Angle75.png diff --git a/test/Common/res/images/brush/linear/mode-bd.png b/test/Common/res/images/brush/linear/ModeBackwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/linear/mode-bd.png rename to test/Common/res/images/brush/linear/ModeBackwardDiagonal.png diff --git a/test/Common/res/images/brush/linear/mode-fd.png b/test/Common/res/images/brush/linear/ModeForwardDiagonal.png similarity index 100% rename from test/Common/res/images/brush/linear/mode-fd.png rename to test/Common/res/images/brush/linear/ModeForwardDiagonal.png diff --git a/test/Common/res/images/brush/linear/mode-h.png b/test/Common/res/images/brush/linear/ModeHorizontal.png similarity index 100% rename from test/Common/res/images/brush/linear/mode-h.png rename to test/Common/res/images/brush/linear/ModeHorizontal.png diff --git a/test/Common/res/images/brush/linear/angle-90.png b/test/Common/res/images/brush/linear/ModeVertical.png similarity index 100% rename from test/Common/res/images/brush/linear/angle-90.png rename to test/Common/res/images/brush/linear/ModeVertical.png diff --git a/test/Common/res/images/brush/linear/mode-v.png b/test/Common/res/images/brush/linear/angle90.png similarity index 100% rename from test/Common/res/images/brush/linear/mode-v.png rename to test/Common/res/images/brush/linear/angle90.png diff --git a/test/Common/res/images/brush/solid/color-blue.png b/test/Common/res/images/brush/solid/ColorBlue.png similarity index 100% rename from test/Common/res/images/brush/solid/color-blue.png rename to test/Common/res/images/brush/solid/ColorBlue.png diff --git a/test/Common/res/images/brush/solid/ColorGreen.png b/test/Common/res/images/brush/solid/ColorGreen.png new file mode 100644 index 0000000000000000000000000000000000000000..3be55607bd26777ac62b6345e68e9991dd905b8f GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V5O&vV@QVc-4lk43!lvI6;>1s;*b3=DjSL74G){)!Z!V78}=V@QVc-D8G=3xv^uZgA8CTA;I0 Date: Mon, 5 Aug 2024 16:14:25 -0300 Subject: [PATCH 087/217] add matrix comparison in LinearGradientBrush unit test for Clone method --- test/Common/Drawing2D/LinearGradientBrushUnitTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs index e45f918..b916d02 100644 --- a/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs +++ b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs @@ -93,6 +93,7 @@ public void Method_Clone() Assert.That(brush2, Is.Not.Null); Assert.That(brush2, Is.Not.SameAs(brush1)); Assert.That(brush2.LinearColors, Is.EqualTo(brush1.LinearColors)); + Assert.That(brush2.Transform.Elements, Is.EqualTo(brush1.Transform.Elements)); Assert.That(brush2.Rectangle, Is.EqualTo(brush1.Rectangle)); Assert.That(brush2.WrapMode, Is.EqualTo(brush1.WrapMode)); }); From 403c2b7a5a335a07c614db370a621c94a6f5417c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 5 Aug 2024 22:26:36 -0300 Subject: [PATCH 088/217] fix GraphicsPath.Clone method --- src/Common/Drawing2D/GraphicsPath.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index b74def0..cc8000c 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -72,7 +72,10 @@ public void Dispose() /// Creates an exact copy of this . ///

public object Clone() - => new GraphicsPath(m_path); + { + using var path = new SKPath(m_path); + return new GraphicsPath(path); + } #endregion From 9c5c2a4e4d39a2a6a8b90c7a1f054d91101fbd87 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 5 Aug 2024 22:26:57 -0300 Subject: [PATCH 089/217] fix GraphicsPath.GetBounds method --- src/Common/Drawing2D/GraphicsPath.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index cc8000c..dca83e8 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -636,7 +636,8 @@ public RectangleF GetBounds(Matrix matrix) ///
public RectangleF GetBounds(Matrix matrix, Pen pen) { - using var transformed = new GraphicsPath(m_path); + using var path = new SKPath(m_path); + using var transformed = new GraphicsPath(path); transformed.Transform(matrix); using var fill = pen.m_paint.GetFillPath(transformed.m_path) ?? transformed.m_path; return new RectangleF(fill.Bounds); From aac7c10e512038149e1f7d379e5414e9f3252f85 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 5 Aug 2024 23:43:53 -0300 Subject: [PATCH 090/217] fix GraphicsPath.CreatePath method when bezier curve has odd points count --- src/Common/Drawing2D/GraphicsPath.cs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index dca83e8..127e3ab 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -1064,15 +1064,22 @@ private static SKPath CreatePath(PointF[] points, byte[] types, FillMode mode) break; case PathPointType.Bezier: - if (i + 2 >= points.Length - || (types[i + 1] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier - || (types[i + 2] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier) - throw new ArgumentException("invalid Bezier curve definition."); - - path.CubicTo(points[i].m_point, points[i + 1].m_point, points[i + 2].m_point); - - i += 2; - break; + if (i + 2 < points.Length + && (types[i + 1] & (byte)PathPointType.PathTypeMask) == (byte)PathPointType.Bezier + && (types[i + 2] & (byte)PathPointType.PathTypeMask) == (byte)PathPointType.Bezier) + { + path.CubicTo(points[i].m_point, points[i + 1].m_point, points[i + 2].m_point); + i += 2; + break; + } + if (i + 1 < points.Length + && (types[i + 1] & (byte)PathPointType.PathTypeMask) == (byte)PathPointType.Bezier) + { + path.QuadTo(points[i].m_point, points[i + 1].m_point); + i += 1; + break; + } + throw new ArgumentException("invalid Bezier curve definition."); default: throw new ArgumentException($"unknown type 0x{type:X2} at index {i}", nameof(types)); From 8754213aef4e22a79a24f15d7fa938d1bd2eb4e8 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 5 Aug 2024 23:51:03 -0300 Subject: [PATCH 091/217] fix PathGradientBrush.InterpolationColors property set --- src/Common/Drawing2D/LinearGradientBrush.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 18891fe..6974ef9 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -128,6 +128,8 @@ public ColorBlend InterpolationColors set => UpdateShader(() => { var colors = value ?? throw new ArgumentNullException(nameof(value)); + if (Enumerable.SequenceEqual(m_colors.Positions, colors.Positions) && Enumerable.SequenceEqual(m_colors.Colors, colors.Colors)) + return; if (colors.Positions[0] != 0 ) throw new ArgumentException("first element must be equal to 0.", nameof(value)); if (colors.Positions[value.Positions.Length - 1] != 1) From 3a0d60936aba82ff46bed84af3a5865bf68b395d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 5 Aug 2024 23:56:40 -0300 Subject: [PATCH 092/217] fix GraphicsPath.Clone method --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 127e3ab..09f2c45 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -73,7 +73,7 @@ public void Dispose() ///
public object Clone() { - using var path = new SKPath(m_path); + var path = new SKPath(m_path); return new GraphicsPath(path); } From d3435c93fc2bce21c7a7a50dfd43de569feaee0d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 00:28:39 -0300 Subject: [PATCH 093/217] generalize Brush's SetBlendTriangularShape and SetSigmaBellShape methods --- src/Common/Brush.cs | 131 ++++++++++++++++++++ src/Common/Drawing2D/LinearGradientBrush.cs | 129 +------------------ src/Common/Drawing2D/PathGradientBrush.cs | 4 +- 3 files changed, 135 insertions(+), 129 deletions(-) diff --git a/src/Common/Brush.cs b/src/Common/Brush.cs index 1558e78..c87f8f9 100644 --- a/src/Common/Brush.cs +++ b/src/Common/Brush.cs @@ -64,5 +64,136 @@ protected static Color ApplyFactor(Color color, float factor) (int)(color.G * factor), (int)(color.B * factor)); + protected static Drawing2D.Blend GetBlendTriangularShape(float focus, float scale) + { + if (focus < 0 || focus > 1) + throw new ArgumentException("Invalid focus value", nameof(focus)); + if (scale < 0 || scale > 1) + throw new ArgumentException("Invalid scale value", nameof(scale)); + + int count = focus == 0 || focus == 1 ? 2 : 3; + Drawing2D.Blend blend = new(count); + + if (focus == 0) + { + blend.Positions[0] = 0; + blend.Factors[1] = scale; + blend.Positions[1] = 1; + blend.Factors[1] = 0; + } + else if (focus == 1) + { + blend.Positions[0] = 0; + blend.Factors[1] = 0; + blend.Positions[1] = 1; + blend.Factors[1] = scale; + } + else + { + blend.Positions[0] = 0; + blend.Factors[0] = 0; + blend.Positions[1] = focus; + blend.Factors[1] = scale; + blend.Positions[2] = 1; + blend.Factors[2] = 0; + } + return blend; + } + + protected static Drawing2D.Blend GetSigmaBellShape(float focus, float scale = 1.0f) + { + if (focus < 0 || focus > 1) + throw new ArgumentException("Invalid focus value", nameof(focus)); + if (scale < 0 || scale > 1) + throw new ArgumentException("Invalid scale value", nameof(scale)); + + int count = focus == 0 || focus == 1 ? 256 : 511; + Drawing2D.Blend m_blend = new(count); + + // TODO: clear preset colors + + float fallOffLenght = 2.0f; + if (focus == 0) + { + m_blend.Positions[0] = focus; + m_blend.Factors[0] = scale; + + SigmaBellBlend(ref m_blend, focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, true); + + m_blend.Positions[count - 1] = 1f; + m_blend.Factors[count - 1] = 0f; + } + else if (focus == 1) + { + m_blend.Positions[0] = 0f; + m_blend.Factors[0] = 0f; + + SigmaBellBlend(ref m_blend, focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, false); + + m_blend.Positions[count - 1] = focus; + m_blend.Factors[count - 1] = scale; + } + else + { + int middle = count / 2; + + // left part of the sigma bell + m_blend.Positions[0] = 0f; + m_blend.Factors[0] = 0f; + + SigmaBellBlend(ref m_blend, focus, scale, focus / (2 * fallOffLenght), focus / 2, focus / 255, 1, middle, false); + + // middle part of the sigma bell + m_blend.Positions[middle] = focus; + m_blend.Factors[middle] = scale; + + // right part of the sigma bell + SigmaBellBlend(ref m_blend, focus, scale, (1 - focus) / (2 * fallOffLenght), (1 + focus) / 2, (1 - focus) / 255, middle + 1, count - 1, true); + + m_blend.Positions[count - 1] = 1f; + m_blend.Factors[count - 1] = 0f; + } + return m_blend; + + static void SigmaBellBlend(ref Drawing2D.Blend blend, float focus, float scale, float sigma, float mean, float delta, int startIndex, int endIndex, bool invert) + { + float sg = invert ? -1 : 1; + float x0 = invert ? 1f : 0f; + + float cb = (1 + sg * Erf(x0, sigma, mean)) / 2; + float ct = (1 + sg * Erf(focus, sigma, mean)) / 2; + float ch = ct - cb; + + float offset = invert ? focus : 0; + float pos = delta + offset; + + for (int index = startIndex; index < endIndex; index++, pos += delta) + { + blend.Positions[index] = pos; + blend.Factors[index] = scale / ch * ((1 + sg * Erf(pos, sigma, mean)) / 2 - cb); + } + } + + static float Erf(float x, float sigma, float mean, int terms = 6) + { + /* + * Error function (Erf) for Gaussian distribution by Maclaurin series: + * erf (z) = (2 / sqrt (pi)) * infinite sum of [(pow (-1, n) * pow (z, 2n+1))/(n! * (2n+1))] + */ + float constant = 2 / (float)Math.Sqrt(Math.PI); + float z = (x - mean) / (sigma * (float)Math.Sqrt(2)); + + float series = z; + for (int n = 1, fact = 1; n < terms; n++, fact *= n) + { + int sign = (int)Math.Pow(-1, n); + int step = 2 * n + 1; + series += sign * (float)Math.Pow(z, step) / (fact * step); + } + + return constant * series; + } + } + #endregion } diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 6974ef9..5245b9c 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -225,99 +225,13 @@ public void TranslateTransform(float dx, float dy, MatrixOrder order) /// Creates a gradient falloff based on a bell-shaped curve. /// public void SetSigmaBellShape(float focus, float scale = 1.0f) - => UpdateShader(() => - { - if (focus < 0 || focus > 1) - throw new ArgumentException("Invalid focus value", nameof(focus)); - if (scale < 0 || scale > 1) - throw new ArgumentException("Invalid scale value", nameof(scale)); - - int count = focus == 0 || focus == 1 ? 256 : 511; - m_blend = new(count); - - // TODO: clear preset colors - - float fallOffLenght = 2.0f; - if (focus == 0) - { - m_blend.Positions[0] = focus; - m_blend.Factors[0] = scale; - - SigmaBellBlend(focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, true); - - m_blend.Positions[count - 1] = 1f; - m_blend.Factors[count - 1] = 0f; - } - else if (focus == 1) - { - m_blend.Positions[0] = 0f; - m_blend.Factors[0] = 0f; - - SigmaBellBlend(focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, false); - - m_blend.Positions[count - 1] = focus; - m_blend.Factors[count - 1] = scale; - } - else - { - int middle = count / 2; - - // left part of the sigma bell - m_blend.Positions[0] = 0f; - m_blend.Factors[0] = 0f; - - SigmaBellBlend(focus, scale, focus / (2 * fallOffLenght), focus / 2, focus / 255, 1, middle, false); - - // middle part of the sigma bell - m_blend.Positions[middle] = focus; - m_blend.Factors[middle] = scale; - - // right part of the sigma bell - SigmaBellBlend(focus, scale, (1 - focus) / (2 * fallOffLenght), (1 + focus) / 2, (1 - focus) / 255, middle + 1, count - 1, true); - - m_blend.Positions[count - 1] = 1f; - m_blend.Factors[count - 1] = 0f; - } - }); + => UpdateShader(() => m_blend = GetSigmaBellShape(focus, scale)); /// /// Creates a linear gradient with a center color and a linear falloff to a single color on both ends. /// public void SetBlendTriangularShape(float focus, float scale = 1.0f) - => UpdateShader(() => - { - if (focus < 0 || focus > 1) - throw new ArgumentException("Invalid focus value", nameof(focus)); - if (scale < 0 || scale > 1) - throw new ArgumentException("Invalid scale value", nameof(scale)); - - int count = focus == 0 || focus == 1 ? 2 : 3; - m_blend = new(count); - - if (focus == 0) - { - m_blend.Positions[0] = 0; - m_blend.Factors[1] = scale; - m_blend.Positions[1] = 1; - m_blend.Factors[1] = 0; - } - else if (focus == 1) - { - m_blend.Positions[0] = 0; - m_blend.Factors[1] = 0; - m_blend.Positions[1] = 1; - m_blend.Factors[1] = scale; - } - else - { - m_blend.Positions[0] = 0; - m_blend.Factors[0] = 0; - m_blend.Positions[1] = focus; - m_blend.Factors[1] = scale; - m_blend.Positions[2] = 1; - m_blend.Factors[2] = 0; - } - }); + => UpdateShader(() => m_blend = GetBlendTriangularShape(focus, scale)); #endregion @@ -419,45 +333,6 @@ static float GetSlope(double angleRadians, float aspectRatio) => -1.0f / (aspectRatio * (float)Math.Tan(angleRadians)); } - void SigmaBellBlend(float focus, float scale, float sigma, float mean, float delta, int startIndex, int endIndex, bool invert) - { - float sg = invert ? -1 : 1; - float x0 = invert ? 1f : 0f; - - float cb = (1 + sg * Erf(x0, sigma, mean)) / 2; - float ct = (1 + sg * Erf(focus, sigma, mean)) / 2; - float ch = ct - cb; - - float offset = invert ? focus : 0; - float pos = delta + offset; - - for (int index = startIndex; index < endIndex; index++, pos += delta) - { - m_blend.Positions[index] = pos; - m_blend.Factors[index] = scale / ch * ((1 + sg * Erf(pos, sigma, mean)) / 2 - cb); - } - - static float Erf(float x, float sigma, float mean, int terms = 6) - { - /* - * Error function (Erf) for Gaussian distribution by Maclaurin series: - * erf (z) = (2 / sqrt (pi)) * infinite sum of [(pow (-1, n) * pow (z, 2n+1))/(n! * (2n+1))] - */ - float constant = 2 / (float)Math.Sqrt(Math.PI); - float z = (x - mean) / (sigma * (float)Math.Sqrt(2)); - - float series = z; - for (int n = 1, fact = 1; n < terms; n++, fact *= n) - { - int sign = (int)Math.Pow(-1, n); - int step = 2 * n + 1; - series += sign * (float)Math.Pow(z, step) / (fact * step); - } - - return constant * series; - } - } - private static Color ApplyGamma(Color color, float gamma) => Color.FromArgb( color.A, diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 2522955..31062c5 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -192,14 +192,14 @@ public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.P /// Creates a gradient with a center color and a linear falloff to each surrounding color. /// public void SetBlendTriangularShape(float focus, float scale = 1.0f) - => throw new NotImplementedException(); + => UpdateShader(() => m_blend = GetBlendTriangularShape(focus, scale)); /// /// Creates a gradient brush that changes color starting from the center of the path outward /// to the path's boundary. The transition from one color to another is based on a bell-shaped curve. /// public void SetSigmaBellShape(float focus, float scale = 1.0f) - => throw new NotImplementedException(); + => UpdateShader(() => m_blend = GetSigmaBellShape(focus, scale)); /// /// Translates the local geometric transformation of this object From 84e06c5bd2522ebc0715b217f54da3ae530e9f27 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 00:29:37 -0300 Subject: [PATCH 094/217] fix PathGradientBrush.Clone method --- src/Common/Drawing2D/PathGradientBrush.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 31062c5..625b794 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -63,7 +63,19 @@ public PathGradientBrush(Point[] points, WrapMode mode = WrapMode.Clamp) /// Creates an exact copy of this . /// public override object Clone() - => m_path.Clone() is GraphicsPath path ? new PathGradientBrush(path, m_mode, m_transform) : throw new Exception("path could not be cloned."); + { + var path = new GraphicsPath(m_path.PathPoints, m_path.PathTypes, m_path.FillMode); + var transform = new Matrix(m_transform.MatrixElements); + return new PathGradientBrush(path, WrapMode, transform) + { + Blend = Blend, + CenterColor = CenterColor, + CenterPoint = CenterPoint, + FocusScales = FocusScales, + InterpolationColors = InterpolationColors, + SurroundColors = SurroundColors, + }; + } #endregion From 75b7b4667203c3b29c5675ab5b45307f9a34a80d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 00:29:54 -0300 Subject: [PATCH 095/217] fix PathGradientBrush.InterpolationColors property --- src/Common/Drawing2D/PathGradientBrush.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 625b794..9745e4f 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -125,7 +125,17 @@ public PointF FocusScales public ColorBlend InterpolationColors { get => m_interpolation; - set => UpdateShader(() => m_interpolation = value); + set => UpdateShader(() => + { + var interpolation = value ?? throw new ArgumentNullException(nameof(value)); + if (Enumerable.SequenceEqual(m_interpolation.Positions, interpolation.Positions) && Enumerable.SequenceEqual(m_interpolation.Colors, interpolation.Colors)) + return; + if (interpolation.Positions[0] != 0 ) + throw new ArgumentException("first element must be equal to 0.", nameof(value)); + if (interpolation.Positions[value.Positions.Length - 1] != 1) + throw new ArgumentException("last element must be equal to 1.", nameof(value)); + m_interpolation = interpolation; + }); } /// From 3c5706ed1cffebd3bc2f1a34fff45e2935925e6e Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 00:33:19 -0300 Subject: [PATCH 096/217] fix PathGradientBrush.UpdateShader method and constructor --- src/Common/Drawing2D/PathGradientBrush.cs | 83 ++++++++++++++++------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 9745e4f..8393f76 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using SkiaSharp; @@ -21,18 +22,20 @@ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) { m_path = path; m_mode = mode; + m_transform = transform; + + m_blend = new(); + Array.Copy(new[] { 1f }, m_blend.Factors, 1); + Array.Copy(new[] { 0f }, m_blend.Positions, 1); + + m_interpolation = new(); + Array.Copy(new[] { Color.Empty }, m_interpolation.Colors, 1); + Array.Copy(new[] { 0f }, m_interpolation.Positions, 1); - m_blend = null; m_color = new Color(255, 255, 255, 255); m_center = GetCentroid(m_path); m_focus = new PointF(0, 0); - m_interpolation = new() - { - Colors = new[] { Color.Empty }, - Positions = new[] { 0f } - }; m_surround = new[] { CenterColor }; - m_transform = transform; UpdateShader(() => { }); } @@ -255,33 +258,67 @@ private void UpdateShader(Action action) { action(); + using var transform = new Matrix(m_transform.MatrixElements); switch (m_mode) { case WrapMode.TileFlipX: - m_transform.Scale(-1, 1); + transform.Scale(-1, 1); break; case WrapMode.TileFlipY: - m_transform.Scale(1, -1); + transform.Scale(1, -1); break; case WrapMode.TileFlipXY: - m_transform.Scale(-1, -1); + transform.Scale(-1, -1); break; } - var center = m_center.m_point; - var focus = Math.Max(m_focus.X, m_focus.Y); - var factors = m_blend?.Factors ?? Enumerable.Repeat(1f, m_interpolation.Positions.Length).ToArray(); - var positions = m_interpolation.Positions - .Prepend(0f) - .Take(factors.Length) - .ToArray(); - var colors = m_interpolation.Colors - .Prepend(m_color) - .Zip(factors, ApplyFactor) - .Select(color => color.m_color) - .ToArray(); + var points = new PointF[] { m_center, m_focus }; + transform.TransformPoints(points); + transform.Reset(); + + var matrix = transform.m_matrix; var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; - var matrix = m_transform.m_matrix; + + var center = points[0].m_point; + var focus = Math.Max(points[1].X, points[1].Y); + + int index; + + var blend = new Dictionary() { [0] = m_color, [1] = Color.Empty }; + for (index = 0; index < m_blend.Positions.Length; index++) + { + var pos = m_blend.Positions[index]; + var fac = m_blend.Factors[index]; + blend[pos] = fac; // blend factor + } + for (index = 0; index < m_interpolation.Positions.Length; index++) + { + var pos = m_interpolation.Positions[index]; + var col = m_interpolation.Colors[index]; + blend[pos] = col; // specific color + } + + var positions = blend.Keys.OrderBy(key => key).ToArray(); + var colors = new SKColor[positions.Length]; + + var lastColor = Color.Empty; + for (index = 0; index < positions.Length; index++) + { + var key = positions[index]; + var value = blend[key]; + if (value is Color currColor) + { + colors[index] = currColor.m_color; + lastColor = currColor; + continue; + } + if (value is float factor) + { + var color = ApplyFactor(lastColor, factor); + colors[index] = color.m_color; + continue; + } + } m_paint.Shader = SKShader.CreateRadialGradient(center, focus, colors, positions, mode, matrix); } From c622ae3bc1ad7926cab179fa8e37d7b059369870 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 12:04:23 -0300 Subject: [PATCH 097/217] fix LinearGradientBrush.UpdateShader method --- src/Common/Drawing2D/LinearGradientBrush.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 5245b9c..3a02834 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -371,13 +371,13 @@ private void UpdateShader(Action action) int index; var blend = new Dictionary(); - for (index = 0; index < m_blend.Positions.Length; index++) + for (index = 0; index < m_blend.Positions.Length && m_blend.Positions.Length > 1; index++) { var pos = m_blend.Positions[index]; var fac = m_blend.Factors[index]; blend[pos] = fac; // blend factor } - for (index = 0; index < m_colors.Positions.Length; index++) + for (index = 0; index < m_colors.Positions.Length && m_colors.Positions.Length > 1; index++) { var pos = m_colors.Positions[index]; var col = m_colors.Colors[index]; From e6eadcc411b98d29d43d93f962a4dfd1f39f35e2 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 14:57:19 -0300 Subject: [PATCH 098/217] add missing PathGradientBrush constructors --- src/Common/Drawing2D/PathGradientBrush.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 8393f76..a5d5571 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -47,16 +47,28 @@ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) public PathGradientBrush(GraphicsPath path) : this(path, WrapMode.Clamp, new Matrix()) { } + /// + /// Initializes a new instance of the class with the specified points. + /// + public PathGradientBrush(params PointF[] points) + : this(points, WrapMode.Clamp) { } + /// /// Initializes a new instance of the class with the specified points and wrap mode. /// - public PathGradientBrush(PointF[] points, WrapMode mode = WrapMode.Clamp) + public PathGradientBrush(PointF[] points, WrapMode mode) : this(CreatePath(points), mode, new Matrix()) { } + /// + /// Initializes a new instance of the class with the specified points. + /// + public PathGradientBrush(params Point[] points) + : this(points, WrapMode.Clamp) { } + /// /// Initializes a new instance of the class with the specified points and wrap mode. /// - public PathGradientBrush(Point[] points, WrapMode mode = WrapMode.Clamp) + public PathGradientBrush(Point[] points, WrapMode mode) : this(Array.ConvertAll(points, point => new PointF(point.m_point)), mode) { } From 19c45d1eb45733228033ebcbb2fe3e71036a5fe1 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 15:02:23 -0300 Subject: [PATCH 099/217] rename m_interpolation by m_color in PathGradientBrush class --- src/Common/Drawing2D/PathGradientBrush.cs | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index a5d5571..9d17b74 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -12,14 +12,14 @@ public sealed class PathGradientBrush : Brush private WrapMode m_mode; private Matrix m_transform; private Blend m_blend; - private ColorBlend m_interpolation; + private ColorBlend m_colors; private PointF m_center, m_focus; private Color m_color; private Color[] m_surround; private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) - : base(new SKPaint { }) - { + : base(new SKPaint { }) + { m_path = path; m_mode = mode; m_transform = transform; @@ -28,9 +28,9 @@ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) Array.Copy(new[] { 1f }, m_blend.Factors, 1); Array.Copy(new[] { 0f }, m_blend.Positions, 1); - m_interpolation = new(); - Array.Copy(new[] { Color.Empty }, m_interpolation.Colors, 1); - Array.Copy(new[] { 0f }, m_interpolation.Positions, 1); + m_colors = new(); + Array.Copy(new[] { Color.Empty }, m_colors.Colors, 1); + Array.Copy(new[] { 0f }, m_colors.Positions, 1); m_color = new Color(255, 255, 255, 255); m_center = GetCentroid(m_path); @@ -139,17 +139,17 @@ public PointF FocusScales /// public ColorBlend InterpolationColors { - get => m_interpolation; + get => m_colors; set => UpdateShader(() => { var interpolation = value ?? throw new ArgumentNullException(nameof(value)); - if (Enumerable.SequenceEqual(m_interpolation.Positions, interpolation.Positions) && Enumerable.SequenceEqual(m_interpolation.Colors, interpolation.Colors)) + if (Enumerable.SequenceEqual(m_colors.Positions, interpolation.Positions) && Enumerable.SequenceEqual(m_colors.Colors, interpolation.Colors)) return; if (interpolation.Positions[0] != 0 ) throw new ArgumentException("first element must be equal to 0.", nameof(value)); if (interpolation.Positions[value.Positions.Length - 1] != 1) throw new ArgumentException("last element must be equal to 1.", nameof(value)); - m_interpolation = interpolation; + m_colors = interpolation; }); } @@ -303,10 +303,10 @@ private void UpdateShader(Action action) var fac = m_blend.Factors[index]; blend[pos] = fac; // blend factor } - for (index = 0; index < m_interpolation.Positions.Length; index++) + for (index = 0; index < m_colors.Positions.Length && m_colors.Positions.Length > 1; index++) { - var pos = m_interpolation.Positions[index]; - var col = m_interpolation.Colors[index]; + var pos = m_colors.Positions[index]; + var col = m_colors.Colors[index]; blend[pos] = col; // specific color } From ffca01c4b8c2c239a3c43bb2009924fe3e37b5ba Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 15:03:48 -0300 Subject: [PATCH 100/217] make m_color and m_surround independent colors and define center by using MidX/MidY properties in PathGradientBrush --- src/Common/Drawing2D/PathGradientBrush.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 9d17b74..c4e7dc5 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -20,6 +20,9 @@ public sealed class PathGradientBrush : Brush private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) : base(new SKPaint { }) { + var color = Color.White; + var bounds = path.GetBounds().m_rect; + m_path = path; m_mode = mode; m_transform = transform; @@ -32,10 +35,10 @@ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) Array.Copy(new[] { Color.Empty }, m_colors.Colors, 1); Array.Copy(new[] { 0f }, m_colors.Positions, 1); - m_color = new Color(255, 255, 255, 255); - m_center = GetCentroid(m_path); + m_color = color; + m_center = new PointF(bounds.MidX, bounds.MidY); m_focus = new PointF(0, 0); - m_surround = new[] { CenterColor }; + m_surround = new[] { color }; UpdateShader(() => { }); } @@ -260,12 +263,6 @@ private static GraphicsPath CreatePath(PointF[] points) return new GraphicsPath(points, types); } - private static PointF GetCentroid(GraphicsPath path) - { - var bounds = path.GetBounds(); - return new PointF(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2); - } - private void UpdateShader(Action action) { action(); From ac5dfab4be2cac42e9af604da9a2aa2299955316 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 16:02:17 -0300 Subject: [PATCH 101/217] add GraphicsPath.FromSvg extra method --- src/Common/Drawing2D/GraphicsPath.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 09f2c45..4a47a3d 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -772,6 +772,12 @@ public void Widen(Pen pen, Matrix matrix = null, float flatness = 0.25f) public string ToSvg() => m_path.ToSvgPathData(); + /// + /// Returns the associated with SVG input string. + /// + public static GraphicsPath FromSvg(string svg) + => new(SKPath.ParseSvgPathData(svg)); + #endregion From 357897226bbd5d2760b4332adda5fca5e288383b Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 6 Aug 2024 16:28:44 -0300 Subject: [PATCH 102/217] add .DS_Store to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 764e5a2..4859bcf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ obj/ /_packages GeneXus.Drawing-DotNet.sln.DotSettings.user /Test/Common/res/images/.out +.DS_Store From 2340f3c39651df6c85c000bd86f62d2d638480e3 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 7 Aug 2024 12:10:32 -0300 Subject: [PATCH 103/217] fix GraphicsPath.AddString method when StringFormatFlags.NoClip is not set and layout parameter is an infinite rectangle --- src/Common/Drawing2D/GraphicsPath.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 4a47a3d..d22bed0 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -1013,9 +1013,13 @@ private void AddString(string text, FontFamily family, int style, float emSize, // apply clip if required if (!format.FormatFlags.HasFlag(StringFormatFlags.NoClip)) { - var bounds = new SKPath(); - bounds.AddRect(layout); - path = path.Op(bounds, SKPathOp.Intersect); + var clip = new SKPath(); + clip.AddRect(new SKRect( + Math.Max(layout.Left, path.TightBounds.Left), + Math.Max(layout.Top, path.TightBounds.Top), + Math.Min(layout.Right, path.TightBounds.Right), + Math.Min(layout.Bottom, path.TightBounds.Bottom))); + path = path.Op(clip, SKPathOp.Intersect); } m_path.AddPath(path); From d768f35573a37d950ec848b295d4cc81ecbd0cb5 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 7 Aug 2024 14:34:28 -0300 Subject: [PATCH 104/217] fix initial center calculation in PathGradientBrush class --- src/Common/Drawing2D/PathGradientBrush.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index c4e7dc5..f5a773d 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -21,7 +21,9 @@ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) : base(new SKPaint { }) { var color = Color.White; - var bounds = path.GetBounds().m_rect; + var points = path.PathPoints; + if (points.First() == points.Last()) + points = points.Take(points.Length - 1).ToArray(); m_path = path; m_mode = mode; @@ -35,8 +37,16 @@ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) Array.Copy(new[] { Color.Empty }, m_colors.Colors, 1); Array.Copy(new[] { 0f }, m_colors.Positions, 1); + m_center = new PointF(0, 0); + foreach (var point in points) + { + m_center.X += point.X; + m_center.Y += point.Y; + } + m_center.X /= points.Length; + m_center.Y /= points.Length; + m_color = color; - m_center = new PointF(bounds.MidX, bounds.MidY); m_focus = new PointF(0, 0); m_surround = new[] { color }; From d46cff8482f70eb33739a1b56460ee5035811569 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 7 Aug 2024 14:41:11 -0300 Subject: [PATCH 105/217] implement circle vs ellipsis in PathGradientBrush class and fix radius calculation --- src/Common/Drawing2D/PathGradientBrush.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index f5a773d..09cf1e2 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -339,7 +339,14 @@ private void UpdateShader(Action action) } } - m_paint.Shader = SKShader.CreateRadialGradient(center, focus, colors, positions, mode, matrix); + var bounds = m_path.GetBounds(); + float scaleX = bounds.Width < bounds.Height ? bounds.Width / bounds.Height : 1; + float scaleY = bounds.Height < bounds.Width ? bounds.Height / bounds.Width : 1; + transform.Scale(scaleX, scaleY); // make an ellipse + + float radius = Math.Max(bounds.Width, bounds.Height) / 2; + + m_paint.Shader = SKShader.CreateRadialGradient(center, radius, colors, positions, mode, matrix); } #endregion From 84b8f250e988271b12cea2a1d5f8fa70b5bf6741 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 7 Aug 2024 16:08:40 -0300 Subject: [PATCH 106/217] fix PathGradientBrush.CreatePath method to create a closed shape --- src/Common/Drawing2D/PathGradientBrush.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 09cf1e2..f7ce0a1 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -267,9 +267,9 @@ private static GraphicsPath CreatePath(PointF[] points) { var types = new byte[points.Length]; types[0] = (byte)PathPointType.Start; - for (int i = 1; i < types.Length - 1; i++) + for (int i = 1; i < types.Length; i++) types[i] = (byte)PathPointType.Line; - types[types.Length - 1] = (byte)PathPointType.CloseSubpath; + types[types.Length - 1] |= (byte)PathPointType.CloseSubpath; return new GraphicsPath(points, types); } From abedc218f9f241388c8d9edb886b19767ea5dc47 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 7 Aug 2024 16:34:28 -0300 Subject: [PATCH 107/217] fix PathGradientBrush.UpdateShader method to consider ellipse gradient --- src/Common/Drawing2D/PathGradientBrush.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index f7ce0a1..bb6e7a5 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -295,9 +295,7 @@ private void UpdateShader(Action action) transform.TransformPoints(points); transform.Reset(); - var matrix = transform.m_matrix; var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; - var center = points[0].m_point; var focus = Math.Max(points[1].X, points[1].Y); @@ -340,10 +338,16 @@ private void UpdateShader(Action action) } var bounds = m_path.GetBounds(); + + float transX = bounds.Width < bounds.Height ? bounds.Left : 0; + float transY = bounds.Height < bounds.Width ? bounds.Top : 0; + transform.Translate(transX, transY); + float scaleX = bounds.Width < bounds.Height ? bounds.Width / bounds.Height : 1; float scaleY = bounds.Height < bounds.Width ? bounds.Height / bounds.Width : 1; - transform.Scale(scaleX, scaleY); // make an ellipse - + transform.Scale(scaleX, scaleY); // as an ellipse + + var matrix = transform.m_matrix; float radius = Math.Max(bounds.Width, bounds.Height) / 2; m_paint.Shader = SKShader.CreateRadialGradient(center, radius, colors, positions, mode, matrix); From 9c6d7c459b06620696c07bcc38722fe7963c7508 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 8 Aug 2024 11:07:23 -0300 Subject: [PATCH 108/217] close open figures in GraphicsPath class --- src/Common/Drawing2D/GraphicsPath.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index d22bed0..e6288bf 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -873,7 +873,10 @@ private void AddCurve(SKPoint[] points, float tension, bool closed) private void AddEllipse(SKRect rect) - => m_path.AddOval(rect); + { + m_path.AddOval(rect); + m_path.Close(); + } private void AddLine(SKPoint pt1, SKPoint pt2) @@ -897,11 +900,15 @@ private void AddPolygon(SKPoint[] points) if (points.Length < 3) throw new ArgumentException("At least three points are required."); m_path.AddPoly(points, true); + m_path.Close(); } private void AddRectangle(SKRect rect) - => m_path.AddRect(rect); + { + m_path.AddRect(rect); + m_path.Close(); + } private void AddString(string text, FontFamily family, int style, float emSize, SKRect layout, StringFormat format) From e509af98553f2d2a452e3d94ca5cffe292179472 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 8 Aug 2024 11:43:58 -0300 Subject: [PATCH 109/217] minor change in GraphicsPathUnitTest for simplicity --- test/Common/Drawing2D/GraphicsPathUnitTest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/Common/Drawing2D/GraphicsPathUnitTest.cs b/test/Common/Drawing2D/GraphicsPathUnitTest.cs index fd8c24b..194823e 100644 --- a/test/Common/Drawing2D/GraphicsPathUnitTest.cs +++ b/test/Common/Drawing2D/GraphicsPathUnitTest.cs @@ -372,8 +372,10 @@ public void Method_AddString() { var text = "x!"; var font = new Font("Arial", 16); - var origin = new PointF(0, 0); var format = new StringFormat(); + var origin = new PointF(0, 0); + var size = new SizeF(12, 12); + var layout = new RectangleF(origin, size); using var path = new GraphicsPath(); path.AddString(text, font.FontFamily, (int)font.Style, font.Size, origin, format); From cd2f44e3c081ae3313d94e2222b0e6cfae2d3fdd Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 15 Aug 2024 14:55:34 -0300 Subject: [PATCH 110/217] minor typo fix in comment --- test/Common/Drawing2D/HatchBrushUnitTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Common/Drawing2D/HatchBrushUnitTest.cs b/test/Common/Drawing2D/HatchBrushUnitTest.cs index 5c95bb1..ac5836c 100644 --- a/test/Common/Drawing2D/HatchBrushUnitTest.cs +++ b/test/Common/Drawing2D/HatchBrushUnitTest.cs @@ -78,7 +78,7 @@ public void Constructor_StyleForeBack(HatchStyle style) string name = style switch { - HatchStyle.Cross => "Cross", // defines LargeGrid nad Max aliases + HatchStyle.Cross => "Cross", // defines LargeGrid and Max aliases HatchStyle.Horizontal => "Horizontal", // defines Min alias _ => style.ToString() }; From 89e969f85a1323352f1d3e4228bcd6954f177d2c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 15 Aug 2024 15:00:26 -0300 Subject: [PATCH 111/217] adjust image similarity threshold in unit-tests because of antialiasing --- test/Common/Drawing2D/HatchBrushUnitTest.cs | 2 +- test/Common/Drawing2D/LinearGradientBrushUnitTest.cs | 4 ++-- test/Common/SolidBrushUnitTest.cs | 2 +- test/Common/TextureBrushUnitTest.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Common/Drawing2D/HatchBrushUnitTest.cs b/test/Common/Drawing2D/HatchBrushUnitTest.cs index ac5836c..7d3c74f 100644 --- a/test/Common/Drawing2D/HatchBrushUnitTest.cs +++ b/test/Common/Drawing2D/HatchBrushUnitTest.cs @@ -84,7 +84,7 @@ public void Constructor_StyleForeBack(HatchStyle style) }; string path = Path.Combine("brush", "hatch", $"Style{name}.png"); float similarity = Utils.CompareImage(path, brush, true); - Assert.That(similarity, Is.GreaterThan(0.95)); + Assert.That(similarity, Is.GreaterThan(0.9)); }); } diff --git a/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs index b916d02..dd09fa6 100644 --- a/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs +++ b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs @@ -53,7 +53,7 @@ public void Constructor_RectangleMode(LinearGradientMode mode, float[] elements) string path = Path.Combine("brush", "linear", $"Mode{mode}.png"); float similarity = Utils.CompareImage(path, brush, true); - Assert.That(similarity, Is.GreaterThan(0.95)); + Assert.That(similarity, Is.GreaterThan(0.9)); }); } @@ -79,7 +79,7 @@ public void Constructor_RectangleAngle(float angle, float[] elements) string path = Path.Combine("brush", "linear", $"Angle{angle}.png"); float similarity = Utils.CompareImage(path, brush, true); - Assert.That(similarity, Is.GreaterThan(0.95)); + Assert.That(similarity, Is.GreaterThan(0.9)); }); } diff --git a/test/Common/SolidBrushUnitTest.cs b/test/Common/SolidBrushUnitTest.cs index b81aea7..4f74d4d 100644 --- a/test/Common/SolidBrushUnitTest.cs +++ b/test/Common/SolidBrushUnitTest.cs @@ -24,7 +24,7 @@ public void Constructor_Color(string name) string path = Path.Combine("brush", "solid", $"Color{name}.png"); float similarity = Utils.CompareImage(path, brush, true); - Assert.That(similarity, Is.GreaterThan(0.95)); + Assert.That(similarity, Is.GreaterThan(0.9)); }); } diff --git a/test/Common/TextureBrushUnitTest.cs b/test/Common/TextureBrushUnitTest.cs index 9ab19ef..7154f4c 100644 --- a/test/Common/TextureBrushUnitTest.cs +++ b/test/Common/TextureBrushUnitTest.cs @@ -37,7 +37,7 @@ public void Constructor_BitmapRectWrap(WrapMode mode) string path = Path.Combine("brush", "textured", $"Mode{mode}.png"); float similarity = Utils.CompareImage(path, brush, true); - Assert.That(similarity, Is.GreaterThan(0.95)); + Assert.That(similarity, Is.GreaterThan(0.9)); }); } From 954da92f52040c275e7d78063da6841bc90fb2d0 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 16 Aug 2024 09:21:44 -0300 Subject: [PATCH 112/217] compare images by pixel color euclidean distance, add bounding rectangle and center poiint calculation methods based on set of points --- test/Common/Utils.cs | 132 ++++++++++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 46 deletions(-) diff --git a/test/Common/Utils.cs b/test/Common/Utils.cs index f2e13f0..7158635 100644 --- a/test/Common/Utils.cs +++ b/test/Common/Utils.cs @@ -9,52 +9,97 @@ internal abstract class Utils Directory.GetParent(Environment.CurrentDirectory).Parent.FullName, "res", "images"); - public static double DeltaE(Color color1, Color color2) + private static readonly double MAX_COLOR_DISTANCE = GetColorDistance( + Color.FromArgb(0, 0, 0, 0), + Color.FromArgb(255, 255, 255, 255)); + + public static RectangleF GetBoundingRectangle(PointF[] points) { - var lab1 = RgbToLab(color1); - var lab2 = RgbToLab(color2); + if (points == null || points.Length == 0) + throw new ArgumentException("The points array cannot be null or empty.", nameof(points)); + + float minX = points[0].X, maxX = points[0].X, minY = points[0].Y, maxY = points[0].Y; + foreach (PointF point in points) + { + minX = point.X < minX ? point.X : minX; + maxX = point.X > maxX ? point.X : maxX; + minY = point.Y < minY ? point.Y : minY; + maxY = point.Y > maxY ? point.Y : maxY; + } - // human eye cannot distinguish colors below 1 DeltaE - return DistanceCie76(lab1, lab2, color1.A / 255.0, color2.A / 255.0); - } + float width = maxX - minX; + float height = maxY - minY; - private static double[] RgbToLab(Color color) - { - // convert RGB to XYZ - double r = PivotRgb(color.R / 255.0); - double g = PivotRgb(color.G / 255.0); - double b = PivotRgb(color.B / 255.0); - - double x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375; - double y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750; - double z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041; - - // convert XYZ to LAB - double[] xyz = { x / 0.95047, y / 1.00000, z / 1.08883 }; - for (int i = 0; i < 3; i++) - xyz[i] = PivotXyz(xyz[i]); - - return new[] - { - 116.0 * xyz[1] - 16, - 500.0 * (xyz[0] - xyz[1]), - 200.0 * (xyz[1] - xyz[2]) - }; + return new RectangleF(minX, minY, width, height); } - private static double PivotRgb(double n) - => (n > 0.04045) ? Math.Pow((n + 0.055) / 1.055, 2.4) : n / 12.92; + public static PointF GetCenterPoint(PointF[] points) + { + PointF center = new(0, 0); + foreach (PointF point in points) + { + center.X += point.X; + center.Y += point.Y; + } + center.X /= points.Length; + center.Y /= points.Length; + return center; + } + + public static double GetColorDistance(Color color1, Color color2) + { + double aDiff = Math.Pow(color1.A - color2.A, 2); + double rDiff = Math.Pow(color1.R - color2.R, 2); + double gDiff = Math.Pow(color1.G - color2.G, 2); + double bDiff = Math.Pow(color1.B - color2.B, 2); + return Math.Sqrt(aDiff + rDiff + gDiff + bDiff); + } - private static double PivotXyz(double n) - => (n > 0.008856) ? Math.Pow(n, 1.0 / 3.0) : (7.787 * n) + (16.0 / 116.0); + public static Color GetAverageColor(Bitmap bm, int x, int y, int radius) + { + int aSum = 0, rSum = 0, gSum = 0, bSum = 0, count = 0; + for (int dy = -radius; dy <= radius; dy++) + { + for (int dx = -radius; dx <= radius; dx++) + { + int nx = x + dx; + int ny = y + dy; + + if (nx < 0 || ny < 0 || nx >= bm.Width || ny >= bm.Height) + continue; + + Color color = bm.GetPixel(nx, ny); + rSum += color.R; + gSum += color.G; + bSum += color.B; + aSum += color.A; + count++; + } + } + return Color.FromArgb(aSum / count, rSum / count, gSum / count, bSum / count); + } - private static double DistanceCie76(double[] lab1, double[] lab2, double alpha1, double alpha2) - => Math.Sqrt( - Math.Pow(lab2[0] - lab1[0], 2) + - Math.Pow(lab2[1] - lab1[1], 2) + - Math.Pow(lab2[2] - lab1[2], 2) + - Math.Pow(alpha1 - alpha2, 2) - ); + public static float GetSimilarity(Bitmap bm1, Bitmap bm2, double tolerance = 0.1, int window = 3) + { + float hits = 0f; // compare pixel by pixel considering the average in a box of window size + if (bm1.Size == bm2.Size) + { + int radius = window / 2; + double threshold = tolerance * MAX_COLOR_DISTANCE; + + for (int i = 0; i < bm1.Width; i++) + { + for (int j = 0; j < bm1.Height; j++) + { + Color avg1 = GetAverageColor(bm1, i, j, radius); + Color avg2 = GetAverageColor(bm2, i, j, radius); + + hits += GetColorDistance(avg1, avg2) < threshold ? 1 : 0; + } + } + } + return hits / (bm1.Width * bm1.Height); + } public static float CompareImage(string filename, Brush brush, bool save = false) { @@ -67,21 +112,16 @@ public static float CompareImage(string filename, Brush brush, bool save = false using var g = Graphics.FromImage(bg); g.FillRectangle(brush, bg.GetBounds(ref gu)); - float hits = 0f; // compare pixel to pixel - for (int i = 0; i < bg.Width; i++) - for (int j = 0; j < bg.Height; j++) - hits += DeltaE(bg.GetPixel(i, j), bm.GetPixel(i, j)) < 35 ? 1 : 0; - if (save) { string savepath = Path.Combine(IMAGE_PATH, ".out", filename); - var dirpath = Path.GetDirectoryName(savepath); + string dirpath = Path.GetDirectoryName(savepath); if (!Directory.Exists(dirpath)) Directory.CreateDirectory(dirpath); bg.Save(savepath); } - return hits / (bg.Width * bg.Height); + return GetSimilarity(bg, bm); } } \ No newline at end of file From 0691c88a658b327dd459515c195884de2507b0c6 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 16 Aug 2024 09:30:49 -0300 Subject: [PATCH 113/217] fix PathGradientBrush.UpdateShader without transforming center/focus properties --- src/Common/Drawing2D/PathGradientBrush.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index bb6e7a5..54b26cb 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -291,7 +291,7 @@ private void UpdateShader(Action action) break; } - var points = new PointF[] { m_center, m_focus }; + var points = new PointF[] { new(m_center.X, m_center.Y), new(m_focus.X, m_focus.Y) }; transform.TransformPoints(points); transform.Reset(); From 1b4580b1038c5a12e5e213023ce2743754306192 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 16 Aug 2024 09:32:37 -0300 Subject: [PATCH 114/217] fix PathGradientBrush.UpdateShader translation based on center point --- src/Common/Drawing2D/PathGradientBrush.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 54b26cb..3dc312a 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -337,10 +337,10 @@ private void UpdateShader(Action action) } } - var bounds = m_path.GetBounds(); + var bounds = m_path.GetBounds().m_rect; - float transX = bounds.Width < bounds.Height ? bounds.Left : 0; - float transY = bounds.Height < bounds.Width ? bounds.Top : 0; + float transX = (bounds.MidX - center.X) / 2; + float transY = (bounds.MidY - center.Y) / 2; transform.Translate(transX, transY); float scaleX = bounds.Width < bounds.Height ? bounds.Width / bounds.Height : 1; From ef797a9769603c377d4f2190e7610d2ea1e46332 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 19 Aug 2024 15:58:12 -0300 Subject: [PATCH 115/217] fix infinite recursion in Region.IsVisible method --- src/Common/Region.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Region.cs b/src/Common/Region.cs index 5b4a7aa..35c52ca 100644 --- a/src/Common/Region.cs +++ b/src/Common/Region.cs @@ -338,7 +338,7 @@ public bool IsVisible(float x, float y, Graphics g = null) /// this when drawn using the specified (if it is defined). /// public bool IsVisible(PointF point, Graphics g = null) - => IsVisible(PointF.Truncate(point), g); + => IsVisible(new Point(unchecked((int)point.X), unchecked((int)point.Y)), g); /// /// Tests whether the specified structure is contained within From 97cb0169e9454978260d140679aa4408d482466a Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 19 Aug 2024 20:13:01 -0300 Subject: [PATCH 116/217] redefine Ceiling, Trunkcate and Round from PointF struct in Point struct --- src/Common/Point.cs | 15 +++++++++++++++ src/Common/PointF.cs | 15 --------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Common/Point.cs b/src/Common/Point.cs index 75430fd..980284a 100644 --- a/src/Common/Point.cs +++ b/src/Common/Point.cs @@ -148,6 +148,21 @@ public int Y /// public static Point Subtract(Point pt, Size sz) => new(pt.m_point - sz.m_size); + /// + /// Converts a by performing a ceiling operation on all the coordinates. + /// + public static Point Ceiling(PointF value) => new(unchecked((int)Math.Ceiling(value.X)), unchecked((int)Math.Ceiling(value.Y))); + + /// + /// Converts a by performing a truncate operation on all the coordinates. + /// + public static Point Truncate(PointF value) => new(unchecked((int)value.X), unchecked((int)value.Y)); + + /// + /// Converts a by performing a round operation on all the coordinates. + /// + public static PointF Round(PointF value) => new(unchecked((int)Math.Round(value.X)), unchecked((int)Math.Round(value.Y))); + /// /// Translates this by the specified amount. /// diff --git a/src/Common/PointF.cs b/src/Common/PointF.cs index 77cbab9..41a064c 100644 --- a/src/Common/PointF.cs +++ b/src/Common/PointF.cs @@ -148,21 +148,6 @@ public float Y /// public static PointF Subtract(PointF pt, SizeF sz) => new(pt.m_point - sz.m_size); - /// - /// Converts a by performing a ceiling operation on all the coordinates. - /// - public static PointF Ceiling(PointF value) => new(unchecked((int)Math.Ceiling(value.X)), unchecked((int)Math.Ceiling(value.Y))); - - /// - /// Converts a by performing a truncate operation on all the coordinates. - /// - public static PointF Truncate(PointF value) => new(unchecked((int)value.X), unchecked((int)value.Y)); - - /// - /// Converts a by performing a round operation on all the coordinates. - /// - public static PointF Round(PointF value) => new(unchecked((int)Math.Round(value.X)), unchecked((int)Math.Round(value.Y))); - /// /// Translates this by the specified amount. /// From 50de4707e666e80cdce205842793205ebd50f88f Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 19 Aug 2024 20:13:51 -0300 Subject: [PATCH 117/217] fix Region.IsVisible based on Point.Truncate method --- src/Common/Region.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Region.cs b/src/Common/Region.cs index 35c52ca..86878ef 100644 --- a/src/Common/Region.cs +++ b/src/Common/Region.cs @@ -338,7 +338,7 @@ public bool IsVisible(float x, float y, Graphics g = null) /// this when drawn using the specified (if it is defined). /// public bool IsVisible(PointF point, Graphics g = null) - => IsVisible(new Point(unchecked((int)point.X), unchecked((int)point.Y)), g); + => IsVisible(Point.Truncate(point), g); /// /// Tests whether the specified structure is contained within From 62974f5bb0d528ae6da8ed00af7006a0ae7793ed Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 19 Aug 2024 20:25:26 -0300 Subject: [PATCH 118/217] add Vector2 related methods to PointF struct --- src/Common/PointF.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Common/PointF.cs b/src/Common/PointF.cs index 41a064c..b95461f 100644 --- a/src/Common/PointF.cs +++ b/src/Common/PointF.cs @@ -1,4 +1,5 @@ using System; +using System.Numerics; using SkiaSharp; namespace GeneXus.Drawing; @@ -31,6 +32,13 @@ public PointF(SizeF sz) public PointF(int dw) : this(unchecked((short)((dw >> 0) & 0xFFFF)), unchecked((short)((dw >> 16) & 0xFFFF))) { } + /// + /// Initializes a new instance of the struct from the specified + /// . + /// + public PointF(Vector2 vector) + : this(vector.X, vector.Y) { } + /// /// Creates a human-readable string that represents this . /// @@ -49,6 +57,16 @@ public PointF(int dw) /// public static explicit operator SizeF(PointF p) => new(p.X, p.Y); + /// + /// Converts the specified to a . + /// + public static explicit operator Vector2(PointF point) => point.ToVector2(); + + /// + /// Converts the specified to a . + /// + public static explicit operator PointF(Vector2 vector) => new(vector); + /// /// Compares two objects. The result specifies whether the values of the /// and properties of the two @@ -138,6 +156,11 @@ public float Y #region Methods + /// + /// Creates a new from this . + /// + public readonly Vector2 ToVector2() => new(m_point.X, m_point.Y); + /// /// Translates a by a given . /// From ac1c398cc14ff0c629ead219adafd62efcac5bb3 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 19 Aug 2024 20:33:13 -0300 Subject: [PATCH 119/217] update unit tests for Point and PointF after migrating Ceiling, Round and Truncate methods from PointF struct to Point struct --- test/Common/PointFUnitTest.cs | 36 ----------------------------------- test/Common/PointUnitTest.cs | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/test/Common/PointFUnitTest.cs b/test/Common/PointFUnitTest.cs index cab61f0..9cbd5a9 100644 --- a/test/Common/PointFUnitTest.cs +++ b/test/Common/PointFUnitTest.cs @@ -149,40 +149,4 @@ public void Method_Offset_Point() Assert.That(point.Y, Is.EqualTo(15f)); }); } - - [Test] - public void Static_Method_Ceiling() - { - var point = new PointF(10.4f, 20.6f); - var result = PointF.Ceiling(point); - Assert.Multiple(() => - { - Assert.That(result.X, Is.EqualTo(11f)); - Assert.That(result.Y, Is.EqualTo(21f)); - }); - } - - [Test] - public void Static_Method_Truncate() - { - var point = new PointF(10.9f, 20.6f); - var result = PointF.Truncate(point); - Assert.Multiple(() => - { - Assert.That(result.X, Is.EqualTo(10f)); - Assert.That(result.Y, Is.EqualTo(20f)); - }); - } - - [Test] - public void Static_Method_Round() - { - var point = new PointF(10.4f, 20.6f); - var result = PointF.Round(point); - Assert.Multiple(() => - { - Assert.That(result.X, Is.EqualTo(10f)); - Assert.That(result.Y, Is.EqualTo(21f)); - }); - } } diff --git a/test/Common/PointUnitTest.cs b/test/Common/PointUnitTest.cs index 6503dff..8fa7bb5 100644 --- a/test/Common/PointUnitTest.cs +++ b/test/Common/PointUnitTest.cs @@ -150,4 +150,40 @@ public void Method_Offset_Point() Assert.That(point.Y, Is.EqualTo(15f)); }); } + + [Test] + public void Static_Method_Ceiling() + { + var point = new PointF(10.4f, 20.6f); + var result = Point.Ceiling(point); + Assert.Multiple(() => + { + Assert.That(result.X, Is.EqualTo(11)); + Assert.That(result.Y, Is.EqualTo(21)); + }); + } + + [Test] + public void Static_Method_Truncate() + { + var point = new PointF(10.9f, 20.6f); + var result = Point.Truncate(point); + Assert.Multiple(() => + { + Assert.That(result.X, Is.EqualTo(10)); + Assert.That(result.Y, Is.EqualTo(20)); + }); + } + + [Test] + public void Static_Method_Round() + { + var point = new PointF(10.4f, 20.6f); + var result = Point.Round(point); + Assert.Multiple(() => + { + Assert.That(result.X, Is.EqualTo(10)); + Assert.That(result.Y, Is.EqualTo(21)); + }); + } } \ No newline at end of file From d455619ceb18a10d739a158d5cf75f4e1e3ff3a6 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 19 Aug 2024 20:36:53 -0300 Subject: [PATCH 120/217] add unit tests for Vector2 related methods in PointF struct --- test/Common/PointFUnitTest.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/Common/PointFUnitTest.cs b/test/Common/PointFUnitTest.cs index 9cbd5a9..8b45a67 100644 --- a/test/Common/PointFUnitTest.cs +++ b/test/Common/PointFUnitTest.cs @@ -1,3 +1,5 @@ +using System.Numerics; + namespace GeneXus.Drawing.Test; internal class PointFUnitTest @@ -67,6 +69,18 @@ public void Constructor_Int() }); } + [Test] + public void Constructor_Vector2() + { + var vector = new Vector2(10f, 20f); + var point = new PointF(vector); + Assert.Multiple(() => + { + Assert.That(point.X, Is.EqualTo(vector.X)); + Assert.That(point.Y, Is.EqualTo(vector.Y)); + }); + } + [Test] public void Operator_Equality() { @@ -149,4 +163,16 @@ public void Method_Offset_Point() Assert.That(point.Y, Is.EqualTo(15f)); }); } + + [Test] + public void Method_ToVector2() + { + var point = new PointF(10f, 20f); + var vector = point.ToVector2(); + Assert.Multiple(() => + { + Assert.That(vector.X, Is.EqualTo(point.X)); + Assert.That(vector.Y, Is.EqualTo(point.Y)); + }); + } } From ccb44d249d25bb9dc0464ef814768b49e09286f1 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 20 Aug 2024 16:14:35 -0300 Subject: [PATCH 121/217] fix possible null Matrix parameter value in GraphicsPath.Flatten method --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index e6288bf..50af137 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -547,7 +547,7 @@ public void Flatten(Matrix matrix, float flatness = 0.25f) return; var data = PathData; - matrix.TransformPoints(data.Points); + matrix?.TransformPoints(data.Points); using var path = new GraphicsPath(data.Points, data.Types, FillMode); From c6a6c3132981608952dae4d52a12aff83579c00e Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 21 Aug 2024 17:09:35 -0300 Subject: [PATCH 122/217] fix Clone method in LinearGradientBrush and PathGradientBrush classes --- src/Common/Drawing2D/LinearGradientBrush.cs | 4 +--- src/Common/Drawing2D/PathGradientBrush.cs | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 3a02834..a59c02d 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -128,11 +128,9 @@ public ColorBlend InterpolationColors set => UpdateShader(() => { var colors = value ?? throw new ArgumentNullException(nameof(value)); - if (Enumerable.SequenceEqual(m_colors.Positions, colors.Positions) && Enumerable.SequenceEqual(m_colors.Colors, colors.Colors)) - return; if (colors.Positions[0] != 0 ) throw new ArgumentException("first element must be equal to 0.", nameof(value)); - if (colors.Positions[value.Positions.Length - 1] != 1) + if (colors.Positions[colors.Positions.Length - 1] != 1 && colors.Positions.Length > 1) throw new ArgumentException("last element must be equal to 1.", nameof(value)); m_colors = colors; }); diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 3dc312a..6dd1364 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -156,11 +156,9 @@ public ColorBlend InterpolationColors set => UpdateShader(() => { var interpolation = value ?? throw new ArgumentNullException(nameof(value)); - if (Enumerable.SequenceEqual(m_colors.Positions, interpolation.Positions) && Enumerable.SequenceEqual(m_colors.Colors, interpolation.Colors)) - return; - if (interpolation.Positions[0] != 0 ) + if (interpolation.Positions[0] != 0) throw new ArgumentException("first element must be equal to 0.", nameof(value)); - if (interpolation.Positions[value.Positions.Length - 1] != 1) + if (interpolation.Positions[interpolation.Positions.Length - 1] != 1 && interpolation.Positions.Length > 1) throw new ArgumentException("last element must be equal to 1.", nameof(value)); m_colors = interpolation; }); From 0579daaf06aef59993c27d722cf0a7223501cfb3 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 21 Aug 2024 17:13:51 -0300 Subject: [PATCH 123/217] minor change --- src/Common/Drawing2D/LinearGradientBrush.cs | 8 +++----- src/Common/Drawing2D/PathGradientBrush.cs | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index a59c02d..c5523bb 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -366,16 +366,14 @@ private void UpdateShader(Action action) var gamma = m_gamma ? 2.2f : 1.0f; var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; - int index; - var blend = new Dictionary(); - for (index = 0; index < m_blend.Positions.Length && m_blend.Positions.Length > 1; index++) + for (int index = 0; index < m_blend.Positions.Length && m_blend.Positions.Length > 1; index++) { var pos = m_blend.Positions[index]; var fac = m_blend.Factors[index]; blend[pos] = fac; // blend factor } - for (index = 0; index < m_colors.Positions.Length && m_colors.Positions.Length > 1; index++) + for (int index = 0; index < m_colors.Positions.Length && m_colors.Positions.Length > 1; index++) { var pos = m_colors.Positions[index]; var col = m_colors.Colors[index]; @@ -386,7 +384,7 @@ private void UpdateShader(Action action) var colors = new SKColor[positions.Length]; var lastColor = Color.Empty; - for (index = 0; index < positions.Length; index++) + for (int index = 0; index < positions.Length; index++) { var key = positions[index]; var value = blend[key]; diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 6dd1364..852c06e 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -297,16 +297,14 @@ private void UpdateShader(Action action) var center = points[0].m_point; var focus = Math.Max(points[1].X, points[1].Y); - int index; - var blend = new Dictionary() { [0] = m_color, [1] = Color.Empty }; - for (index = 0; index < m_blend.Positions.Length; index++) + for (int index = 0; index < m_blend.Positions.Length && m_blend.Positions.Length > 1; index++) { var pos = m_blend.Positions[index]; var fac = m_blend.Factors[index]; blend[pos] = fac; // blend factor } - for (index = 0; index < m_colors.Positions.Length && m_colors.Positions.Length > 1; index++) + for (int index = 0; index < m_colors.Positions.Length && m_colors.Positions.Length > 1; index++) { var pos = m_colors.Positions[index]; var col = m_colors.Colors[index]; @@ -317,7 +315,7 @@ private void UpdateShader(Action action) var colors = new SKColor[positions.Length]; var lastColor = Color.Empty; - for (index = 0; index < positions.Length; index++) + for (int index = 0; index < positions.Length; index++) { var key = positions[index]; var value = blend[key]; From 3ddef778b44a7fab62ca52a3ac78da3cf316995d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 22 Aug 2024 16:36:51 -0300 Subject: [PATCH 124/217] minor identation change in Region class --- src/Common/Region.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Region.cs b/src/Common/Region.cs index 86878ef..6ebde67 100644 --- a/src/Common/Region.cs +++ b/src/Common/Region.cs @@ -490,7 +490,7 @@ private static SKRegion ParseRegionData(RegionData data) var rect = new SKRectI { Left = BitConverter.ToInt32(rgnData, offset + 0), - Top = BitConverter.ToInt32(rgnData, offset + 4), + Top = BitConverter.ToInt32(rgnData, offset + 4), Right = BitConverter.ToInt32(rgnData, offset + 8), Bottom = BitConverter.ToInt32(rgnData, offset + 12) }; From fa8747f369985f82cf7094b619a424ac6cabcd52 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 12:01:13 -0300 Subject: [PATCH 125/217] fix PathGradientBrush.CenterColor default depending if SurroundColors was set or not, and include some minor improvements --- src/Common/Drawing2D/PathGradientBrush.cs | 25 ++++++++--------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 852c06e..a1667ae 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -14,13 +14,12 @@ public sealed class PathGradientBrush : Brush private Blend m_blend; private ColorBlend m_colors; private PointF m_center, m_focus; - private Color m_color; + private Color? m_color; private Color[] m_surround; private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) : base(new SKPaint { }) { - var color = Color.White; var points = path.PathPoints; if (points.First() == points.Last()) points = points.Take(points.Length - 1).ToArray(); @@ -37,18 +36,12 @@ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) Array.Copy(new[] { Color.Empty }, m_colors.Colors, 1); Array.Copy(new[] { 0f }, m_colors.Positions, 1); - m_center = new PointF(0, 0); - foreach (var point in points) - { - m_center.X += point.X; - m_center.Y += point.Y; - } - m_center.X /= points.Length; - m_center.Y /= points.Length; + m_center = new( + points.Average(pt => pt.X), + points.Average(pt => pt.Y)); - m_color = color; - m_focus = new PointF(0, 0); - m_surround = new[] { color }; + m_focus = new(0, 0); + m_surround = new[] { Color.White }; UpdateShader(() => { }); } @@ -114,7 +107,7 @@ public override object Clone() /// Gets or sets a that specifies positions and factors that define a /// custom falloff for the gradient. /// - public Blend Blend + public Blend Blend { get => m_blend; set => UpdateShader(() => m_blend = value ?? throw new ArgumentNullException(nameof(value))); @@ -123,9 +116,9 @@ public Blend Blend /// /// Gets or sets the at the center of the path gradient. /// - public Color CenterColor + public Color CenterColor { - get => m_color; + get => m_color ?? (m_surround.Length > 1 ? Color.Black : Color.White); set => UpdateShader(() => m_color = value); } From b3a3a65325f9849c88a8a3861d87d5d5635423f4 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 12:01:50 -0300 Subject: [PATCH 126/217] add constraint check in PathGradientBrush.SurroundColors property set --- src/Common/Drawing2D/PathGradientBrush.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index a1667ae..26c3e0b 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -169,7 +169,12 @@ public ColorBlend InterpolationColors public Color[] SurroundColors { get => m_surround; - set => UpdateShader(() => m_surround = value); + set => UpdateShader(() => + { + if (value.Length > m_path.PointCount) + throw new ArgumentException("parameter is not valid.", nameof(value)); + m_surround = value; + }); } /// From 9f3535f7f5c20b91813062c963e6e08b9f893421 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 12:04:33 -0300 Subject: [PATCH 127/217] add support for custom shape gradient in PathGradientBrush extending its functionality beyond just radial gradients --- src/Common/Drawing2D/PathGradientBrush.cs | 182 ++++++++++++++++++---- 1 file changed, 150 insertions(+), 32 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 26c3e0b..8fa0838 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Numerics; using SkiaSharp; namespace GeneXus.Drawing.Drawing2D; @@ -269,6 +270,101 @@ private static GraphicsPath CreatePath(PointF[] points) return new GraphicsPath(points, types); } + private Color ComputeColor(int x, int y, Vector2[] points, PathPointType[] types, Color[] colors, float[] positions) + { + var center = m_center.ToVector2(); + var color = Color.Transparent; + if (m_path.IsVisible(x, y)) + { + var coord = new Vector2(x, y); + if (m_surround.Length > 1) + { + // determine the weights of this point to the corners of the path + var weight = new float[points.Length]; + for (int i = 0; i < points.Length; i++) + { + var p0 = points[(i + 0) % points.Length]; + var p1 = points[(i + 1) % points.Length]; + + var e0 = p1 - p0; + var e1 = coord - p0; + var ep = p0 + e0 * Vector2.Dot(e0, e1) / Vector2.Dot(e0, e0); + + int j = (i + points.Length - 1) % points.Length; + weight[j] = Vector2.Distance(coord, ep); + } + + float total = weight.Sum(); // for normalizing in 0..1 + + // determine the blended color for the given point by weights + color = colors[colors.Length - 1]; + for (int i = 0; i < weight.Length; i++) + { + float amount = weight[i] / total; + int j = Math.Min(i, colors.Length - 1); // NOTE: there could be less colors than vertices + color = Color.Blend(color, colors[j], amount); + } + + // change the colors and positions according to surrounded colors + colors = new[] { color, colors[colors.Length - 1] }; + positions = new[] { 0f, 1f }; + } + + // determine the distance of this point to the edge of the path + float dist = float.MaxValue, dmax = 0f; + for (int i = 0; i < points.Length; i++) + { + var p0 = points[(i + 0) % points.Length]; + var p1 = points[(i + 1) % points.Length]; + var p2 = points[(i + 2) % points.Length]; + + var type = types[i % types.Length]; + switch(type) + { + case PathPointType.Line: + var e0 = p1 - p0; + var e1 = coord - p0; + var ep = p0 + e0 * Vector2.Dot(e0, e1) / Vector2.Dot(e0, e0); + + dist = Math.Min(dist, Vector2.Distance(coord, ep)); + dmax = Math.Max(dmax, Vector2.Distance(center, ep)); + break; + + case PathPointType.Bezier: + for (float t = 0; t < 1; t += 0.01f) + { + var c0 = Vector2.Lerp(p0, p1, t); + var c1 = Vector2.Lerp(p1, p2, t); + var bp = Vector2.Lerp(c0, c1, t); + + dist = Math.Min(dist, Vector2.Distance(coord, bp)); + dmax = Math.Max(dmax, Vector2.Distance(center, bp)); + } + i++; + break; + + default: + throw new ArgumentException($"unknown type 0x{type:X2} at index {i}", nameof(types)); + } + } + + dist /= dmax; // normalized in 0..1 + + // determine the blended color for the given point by positions + color = colors[colors.Length - 1]; + for (int i = 0; i < positions.Length - 1; i++) + { + if (dist >= positions[i] && dist <= positions[i + 1]) + { + float amount = (dist - positions[i]) / (positions[i + 1] - positions[i]); + color = Color.Blend(colors[i], colors[i + 1], amount); + break; + } + } + } + return color; + } + private void UpdateShader(Action action) { action(); @@ -287,30 +383,51 @@ private void UpdateShader(Action action) break; } - var points = new PointF[] { new(m_center.X, m_center.Y), new(m_focus.X, m_focus.Y) }; - transform.TransformPoints(points); - transform.Reset(); - - var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; - var center = points[0].m_point; - var focus = Math.Max(points[1].X, points[1].Y); - - var blend = new Dictionary() { [0] = m_color, [1] = Color.Empty }; - for (int index = 0; index < m_blend.Positions.Length && m_blend.Positions.Length > 1; index++) + var bounds = m_path.GetBounds(); + var points = m_path.PathPoints + .Select(point => point.ToVector2()) + .ToArray(); + var types = m_path.PathTypes + .Select(type => (PathPointType)(type & (byte)PathPointType.PathTypeMask)) + .Skip(1) // skip PathPointType.Start + .ToArray(); + + var blend = new Dictionary() { [0] = m_surround[0], [1] = m_color }; + if (m_surround.Length > 1) { - var pos = m_blend.Positions[index]; - var fac = m_blend.Factors[index]; - blend[pos] = fac; // blend factor + blend[1] ??= Color.Black; + for (int index = 0; index < m_surround.Length; index++) + { + var pos = 1f * index / m_surround.Length; + var col = m_surround[index]; + blend[pos] = col; // corner color + } } - for (int index = 0; index < m_colors.Positions.Length && m_colors.Positions.Length > 1; index++) + else { - var pos = m_colors.Positions[index]; - var col = m_colors.Colors[index]; - blend[pos] = col; // specific color + blend[1] ??= Color.White; + if (m_blend.Positions.Length > 1) + { + for (int index = 0; index < m_blend.Positions.Length; index++) + { + var pos = m_blend.Positions[index]; + var fac = m_blend.Factors[index]; + blend[pos] = fac; // edge factor + } + } + if (m_colors.Positions.Length > 1) + { + for (int index = 0; index < m_colors.Positions.Length; index++) + { + var pos = m_colors.Positions[index]; + var col = m_colors.Colors[index]; + blend[pos] = col; // edge color + } + } } var positions = blend.Keys.OrderBy(key => key).ToArray(); - var colors = new SKColor[positions.Length]; + var colors = new Color[positions.Length]; var lastColor = Color.Empty; for (int index = 0; index < positions.Length; index++) @@ -319,32 +436,33 @@ private void UpdateShader(Action action) var value = blend[key]; if (value is Color currColor) { - colors[index] = currColor.m_color; + colors[index] = currColor; lastColor = currColor; continue; } if (value is float factor) { var color = ApplyFactor(lastColor, factor); - colors[index] = color.m_color; + colors[index] = color; continue; } } - var bounds = m_path.GetBounds().m_rect; - - float transX = (bounds.MidX - center.X) / 2; - float transY = (bounds.MidY - center.Y) / 2; - transform.Translate(transX, transY); - - float scaleX = bounds.Width < bounds.Height ? bounds.Width / bounds.Height : 1; - float scaleY = bounds.Height < bounds.Width ? bounds.Height / bounds.Width : 1; - transform.Scale(scaleX, scaleY); // as an ellipse - + using var bitmap = new Bitmap(bounds.Width + bounds.Left, bounds.Height + bounds.Top); + for (int x = 0; x < bitmap.Width; x++) + { + for (int y = 0; y < bitmap.Height; y++) + { + var color = ComputeColor(x, y, points, types, colors, positions); + bitmap.SetPixel(x, y, color); + } + } + + var source = bitmap.m_bitmap; var matrix = transform.m_matrix; - float radius = Math.Max(bounds.Width, bounds.Height) / 2; + var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; - m_paint.Shader = SKShader.CreateRadialGradient(center, radius, colors, positions, mode, matrix); + m_paint.Shader = SKShader.CreateBitmap(source, mode, mode, matrix); } #endregion From 71cd7959e19ca07119975b2a8c6383d7eab74062 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 12:06:03 -0300 Subject: [PATCH 128/217] rename m_blend property by m_factor in LinearGradientBrush and PathGradientBrush classes --- src/Common/Drawing2D/LinearGradientBrush.cs | 22 +++++++++---------- src/Common/Drawing2D/PathGradientBrush.cs | 24 ++++++++++----------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index c5523bb..8da930c 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -11,7 +11,7 @@ public sealed class LinearGradientBrush : Brush private RectangleF m_rect; private WrapMode m_mode; private Matrix m_transform; - private Blend m_blend; + private Blend m_factors; private ColorBlend m_colors; private bool m_gamma; @@ -24,9 +24,9 @@ private LinearGradientBrush(RectangleF rect, Color[] colors, WrapMode mode, Matr m_gamma = false; - m_blend = new(); - Array.Copy(new[] { 1f }, m_blend.Factors, 1); - Array.Copy(new[] { 0f }, m_blend.Positions, 1); + m_factors = new(); + Array.Copy(new[] { 1f }, m_factors.Factors, 1); + Array.Copy(new[] { 0f }, m_factors.Positions, 1); var uniform = GetUniformArray(colors.Length); @@ -104,8 +104,8 @@ public override object Clone() /// public Blend Blend { - get => m_blend; - set => UpdateShader(() => m_blend = value ?? throw new ArgumentNullException(nameof(value))); + get => m_factors; + set => UpdateShader(() => m_factors = value ?? throw new ArgumentNullException(nameof(value))); } /// @@ -223,13 +223,13 @@ public void TranslateTransform(float dx, float dy, MatrixOrder order) /// Creates a gradient falloff based on a bell-shaped curve. /// public void SetSigmaBellShape(float focus, float scale = 1.0f) - => UpdateShader(() => m_blend = GetSigmaBellShape(focus, scale)); + => UpdateShader(() => m_factors = GetSigmaBellShape(focus, scale)); /// /// Creates a linear gradient with a center color and a linear falloff to a single color on both ends. /// public void SetBlendTriangularShape(float focus, float scale = 1.0f) - => UpdateShader(() => m_blend = GetBlendTriangularShape(focus, scale)); + => UpdateShader(() => m_factors = GetBlendTriangularShape(focus, scale)); #endregion @@ -367,10 +367,10 @@ private void UpdateShader(Action action) var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; var blend = new Dictionary(); - for (int index = 0; index < m_blend.Positions.Length && m_blend.Positions.Length > 1; index++) + for (int index = 0; index < m_factors.Positions.Length && m_factors.Positions.Length > 1; index++) { - var pos = m_blend.Positions[index]; - var fac = m_blend.Factors[index]; + var pos = m_factors.Positions[index]; + var fac = m_factors.Factors[index]; blend[pos] = fac; // blend factor } for (int index = 0; index < m_colors.Positions.Length && m_colors.Positions.Length > 1; index++) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 8fa0838..df9a522 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -12,7 +12,7 @@ public sealed class PathGradientBrush : Brush private GraphicsPath m_path; private WrapMode m_mode; private Matrix m_transform; - private Blend m_blend; + private Blend m_factors; private ColorBlend m_colors; private PointF m_center, m_focus; private Color? m_color; @@ -29,9 +29,9 @@ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) m_mode = mode; m_transform = transform; - m_blend = new(); - Array.Copy(new[] { 1f }, m_blend.Factors, 1); - Array.Copy(new[] { 0f }, m_blend.Positions, 1); + m_factors = new(); + Array.Copy(new[] { 1f }, m_factors.Factors, 1); + Array.Copy(new[] { 0f }, m_factors.Positions, 1); m_colors = new(); Array.Copy(new[] { Color.Empty }, m_colors.Colors, 1); @@ -110,8 +110,8 @@ public override object Clone() /// public Blend Blend { - get => m_blend; - set => UpdateShader(() => m_blend = value ?? throw new ArgumentNullException(nameof(value))); + get => m_factors; + set => UpdateShader(() => m_factors = value ?? throw new ArgumentNullException(nameof(value))); } /// @@ -239,14 +239,14 @@ public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.P /// Creates a gradient with a center color and a linear falloff to each surrounding color. /// public void SetBlendTriangularShape(float focus, float scale = 1.0f) - => UpdateShader(() => m_blend = GetBlendTriangularShape(focus, scale)); + => UpdateShader(() => m_factors = GetBlendTriangularShape(focus, scale)); /// /// Creates a gradient brush that changes color starting from the center of the path outward /// to the path's boundary. The transition from one color to another is based on a bell-shaped curve. /// public void SetSigmaBellShape(float focus, float scale = 1.0f) - => UpdateShader(() => m_blend = GetSigmaBellShape(focus, scale)); + => UpdateShader(() => m_factors = GetSigmaBellShape(focus, scale)); /// /// Translates the local geometric transformation of this object @@ -406,12 +406,12 @@ private void UpdateShader(Action action) else { blend[1] ??= Color.White; - if (m_blend.Positions.Length > 1) + if (m_factors.Positions.Length > 1) { - for (int index = 0; index < m_blend.Positions.Length; index++) + for (int index = 0; index < m_factors.Positions.Length; index++) { - var pos = m_blend.Positions[index]; - var fac = m_blend.Factors[index]; + var pos = m_factors.Positions[index]; + var fac = m_factors.Factors[index]; blend[pos] = fac; // edge factor } } From b2f6c9e087ea3a75018f6e7df46f1fb2063b8f7e Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 12:07:28 -0300 Subject: [PATCH 129/217] add Color.Blend method for mixing two colors --- src/Common/Color.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Common/Color.cs b/src/Common/Color.cs index a828ab0..a3e8e4c 100644 --- a/src/Common/Color.cs +++ b/src/Common/Color.cs @@ -297,6 +297,20 @@ public static Color FromKnownColor(KnownColor color) /// public readonly KnownColor ToKnownColor() => Enum.TryParse(Name, out KnownColor color) ? color : 0; + /// + /// Linearly interpolates two by a given amount clamped between 0 and 1. + /// + public static Color Blend(Color color1, Color color2, float amount) + { + if (amount <= 0) return color1; + if (amount >= 1) return color2; + int r = (int)(color1.R + (color2.R - color1.R) * amount); + int g = (int)(color1.G + (color2.G - color1.G) * amount); + int b = (int)(color1.B + (color2.B - color1.B) * amount); + int a = (int)(color1.A + (color2.A - color1.A) * amount); + return FromArgb(a, r, g, b); + } + #endregion From 98a454eb5e85a4ba100ba3132c238f3cc4811819 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 12:08:56 -0300 Subject: [PATCH 130/217] fix GraphicsPath.IsVisible method due to behavior differences with Skia --- src/Common/Drawing2D/GraphicsPath.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 50af137..6fb7361 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -1043,8 +1043,8 @@ private bool IsOutlineVisible(SKPoint point, SKPaint pen, SKRect? bounds) private bool IsVisible(SKPoint point, SKRect? bounds) { - bool isBoundContained = bounds?.Contains(point) ?? true; - return isBoundContained && m_path.Bounds.Contains(point.X, point.Y); + bool isBoundContained = bounds?.Contains(point) ?? m_path.Bounds.Contains(point.X, point.Y); + return isBoundContained && m_path.Contains(point.X + 0.5f, point.Y + 0.5f); // NOTE: use the next value because skia issue } #endregion From a09937e38730b004dbcb5a8a1e8690e77dca9658 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 12:10:00 -0300 Subject: [PATCH 131/217] simplify test utils module's methods: GetCenterPoint and GetBoundingRectangle --- test/Common/Utils.cs | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/test/Common/Utils.cs b/test/Common/Utils.cs index 7158635..0d135ce 100644 --- a/test/Common/Utils.cs +++ b/test/Common/Utils.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; namespace GeneXus.Drawing.Test; @@ -18,32 +19,18 @@ public static RectangleF GetBoundingRectangle(PointF[] points) if (points == null || points.Length == 0) throw new ArgumentException("The points array cannot be null or empty.", nameof(points)); - float minX = points[0].X, maxX = points[0].X, minY = points[0].Y, maxY = points[0].Y; - foreach (PointF point in points) - { - minX = point.X < minX ? point.X : minX; - maxX = point.X > maxX ? point.X : maxX; - minY = point.Y < minY ? point.Y : minY; - maxY = point.Y > maxY ? point.Y : maxY; - } - - float width = maxX - minX; - float height = maxY - minY; - - return new RectangleF(minX, minY, width, height); + float xMin = points.Min(pt => pt.X); + float xMax = points.Max(pt => pt.X); + float yMin = points.Min(pt => pt.Y); + float yMax = points.Max(pt => pt.Y); + return new(xMin, yMin, xMax - xMin, yMax - yMin); } public static PointF GetCenterPoint(PointF[] points) { - PointF center = new(0, 0); - foreach (PointF point in points) - { - center.X += point.X; - center.Y += point.Y; - } - center.X /= points.Length; - center.Y /= points.Length; - return center; + float xCenter = points.Average(pt => pt.X); + float yCenter = points.Average(pt => pt.Y); + return new(xCenter, yCenter); } public static double GetColorDistance(Color color1, Color color2) From f3a87870ebcb2d322bb62f00ea48c144c7d3f9c9 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 16:42:33 -0300 Subject: [PATCH 132/217] change offset of GraphicsPath.IsVisible method because skia is not considering the path border in the Contains method --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 6fb7361..c1c4637 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -1044,7 +1044,7 @@ private bool IsOutlineVisible(SKPoint point, SKPaint pen, SKRect? bounds) private bool IsVisible(SKPoint point, SKRect? bounds) { bool isBoundContained = bounds?.Contains(point) ?? m_path.Bounds.Contains(point.X, point.Y); - return isBoundContained && m_path.Contains(point.X + 0.5f, point.Y + 0.5f); // NOTE: use the next value because skia issue + return isBoundContained && m_path.Contains(point.X + 0.1f, point.Y + 0.1f); // NOTE: use an offset becase Skia does not consider the path border } #endregion From 40fb959ea24736fe84b9b3cec3c9d92e3249d142 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 17:07:29 -0300 Subject: [PATCH 133/217] minor fix in ComputeColor.PathGradientBrush when calculate distances between the target point and the edge of the path shape --- src/Common/Drawing2D/PathGradientBrush.cs | 29 ++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index df9a522..d26fb16 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -325,20 +325,32 @@ private Color ComputeColor(int x, int y, Vector2[] points, PathPointType[] types var e0 = p1 - p0; var e1 = coord - p0; var ep = p0 + e0 * Vector2.Dot(e0, e1) / Vector2.Dot(e0, e0); - - dist = Math.Min(dist, Vector2.Distance(coord, ep)); - dmax = Math.Max(dmax, Vector2.Distance(center, ep)); + + float distEp = Vector2.Distance(coord, ep); + float dmaxEp = Vector2.Distance(center, ep); + + if (distEp < dist) + { + dist = distEp; + dmax = dmaxEp; + } break; case PathPointType.Bezier: - for (float t = 0; t < 1; t += 0.01f) + for (float t = 0; t <= 1; t += 0.1f) { var c0 = Vector2.Lerp(p0, p1, t); var c1 = Vector2.Lerp(p1, p2, t); - var bp = Vector2.Lerp(c0, c1, t); - - dist = Math.Min(dist, Vector2.Distance(coord, bp)); - dmax = Math.Max(dmax, Vector2.Distance(center, bp)); + var cp = Vector2.Lerp(c0, c1, t); + + var distCp = Vector2.Distance(coord, cp); + var dmaxCp = Vector2.Distance(center, cp); + + if (distCp < dist) + { + dist = distCp; + dmax = dmaxCp; + } } i++; break; @@ -448,6 +460,7 @@ private void UpdateShader(Action action) } } + // NOTE: Skia does not offers path gradient shader, that's why we use a bitmap using var bitmap = new Bitmap(bounds.Width + bounds.Left, bounds.Height + bounds.Top); for (int x = 0; x < bitmap.Width; x++) { From e433d315d3a112a4823de4c1afca6e01d798a267 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 23 Aug 2024 17:08:49 -0300 Subject: [PATCH 134/217] add PathGradientBrush unit tests --- .../Drawing2D/PathGradientBrushUnitTest.cs | 260 ++++++++++++++++++ test/Common/res/images/brush/path/Circle.png | Bin 0 -> 1600 bytes .../images/brush/path/CircleRecentered.png | Bin 0 -> 1777 bytes .../res/images/brush/path/TriangleBlended.png | Bin 0 -> 358 bytes .../res/images/brush/path/TriangleClamp.png | Bin 0 -> 356 bytes .../brush/path/TriangleInterpolated.png | Bin 0 -> 353 bytes .../images/brush/path/TriangleSurrounded.png | Bin 0 -> 1873 bytes .../res/images/brush/path/TriangleTile.png | Bin 0 -> 484 bytes 8 files changed, 260 insertions(+) create mode 100644 test/Common/Drawing2D/PathGradientBrushUnitTest.cs create mode 100644 test/Common/res/images/brush/path/Circle.png create mode 100644 test/Common/res/images/brush/path/CircleRecentered.png create mode 100644 test/Common/res/images/brush/path/TriangleBlended.png create mode 100644 test/Common/res/images/brush/path/TriangleClamp.png create mode 100644 test/Common/res/images/brush/path/TriangleInterpolated.png create mode 100644 test/Common/res/images/brush/path/TriangleSurrounded.png create mode 100644 test/Common/res/images/brush/path/TriangleTile.png diff --git a/test/Common/Drawing2D/PathGradientBrushUnitTest.cs b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs new file mode 100644 index 0000000..f1a5c8c --- /dev/null +++ b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs @@ -0,0 +1,260 @@ +using System.IO; +using GeneXus.Drawing.Drawing2D; + +namespace GeneXus.Drawing.Test.Drawing2D; + +internal class PathGradientBrushUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_Path() + { + var rect = new RectangleF(5, 5, 40, 40); + var color = Color.Red; + // var center = new PointF(15, 15); // NOT WORKING + + using var path = new GraphicsPath(); + path.AddEllipse(rect); + + using var brush = new PathGradientBrush(path) { CenterColor = color }; + Assert.Multiple(() => + { + var bounds = path.GetBounds(); + var center = new PointF(rect.X + rect.Width / 2, rect.Y + rect.Height / 2); + + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp)); + Assert.That(brush.Transform.IsIdentity, Is.True); + Assert.That(brush.FocusScales, Is.EqualTo(new PointF(0, 0))); + Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White })); + + Assert.That(brush.CenterColor, Is.EqualTo(color)); + Assert.That(brush.CenterPoint, Is.EqualTo(center)); + + Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f })); + Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f })); + + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty })); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f })); + + string filepath = Path.Combine("brush", "path", $"Circle.png"); + float similarity = Utils.CompareImage(filepath, brush, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + [TestCase(WrapMode.Tile)] + [TestCase(WrapMode.Clamp)] + public void Constructor_Points(WrapMode mode) + { + var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) }; + var color = Color.Red; + + using var brush = new PathGradientBrush(points, mode) { CenterColor = color }; + Assert.Multiple(() => + { + var bounds = Utils.GetBoundingRectangle(points); + var center = Utils.GetCenterPoint(points); + + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(mode)); + Assert.That(brush.Transform.IsIdentity, Is.True); + Assert.That(brush.FocusScales, Is.EqualTo(PointF.Empty)); + Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White })); + + Assert.That(brush.CenterColor, Is.EqualTo(color)); + Assert.That(brush.CenterPoint, Is.EqualTo(center)); + + Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f })); + Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f })); + + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty })); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f })); + + string filepath = Path.Combine("brush", "path", $"Triangle{mode}.png"); + float similarity = Utils.CompareImage(filepath, brush, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Property_Blend() + { + var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) }; + var color = Color.Red; + var blend = new Blend() + { + Factors = new float[] { 0.1f, 0.9f, 0.1f }, + Positions = new float[] { 0.0f, 0.4f, 1.0f } + }; + + using var brush = new PathGradientBrush(points) { Blend = blend, CenterColor = color }; + Assert.Multiple(() => + { + var bounds = Utils.GetBoundingRectangle(points); + var center = Utils.GetCenterPoint(points); + + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp)); + Assert.That(brush.Transform.IsIdentity, Is.True); + Assert.That(brush.FocusScales, Is.EqualTo(PointF.Empty)); + Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White })); + + Assert.That(brush.CenterColor, Is.EqualTo(color)); + Assert.That(brush.CenterPoint, Is.EqualTo(center)); + + Assert.That(brush.Blend.Factors, Is.EqualTo(blend.Factors)); + Assert.That(brush.Blend.Positions, Is.EqualTo(blend.Positions)); + + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty })); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f })); + + string filepath = Path.Combine("brush", "path", $"TriangleBlended.png"); + float similarity = Utils.CompareImage(filepath, brush, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Property_CenterPoint() + { + var rect = new RectangleF(5, 5, 40, 40); + var color = Color.Red; + var center = new PointF(15, 15); + + using var path = new GraphicsPath(); + path.AddEllipse(rect); + + using var brush = new PathGradientBrush(path) { CenterColor = color, CenterPoint = center }; + Assert.Multiple(() => + { + var bounds = path.GetBounds(); + + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp)); + Assert.That(brush.Transform.IsIdentity, Is.True); + Assert.That(brush.FocusScales, Is.EqualTo(new PointF(0, 0))); + Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White })); + + Assert.That(brush.CenterColor, Is.EqualTo(color)); + Assert.That(brush.CenterPoint, Is.EqualTo(center)); + + Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f })); + Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f })); + + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty })); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f })); + + string filepath = Path.Combine("brush", "path", $"CircleRecentered.png"); + float similarity = Utils.CompareImage(filepath, brush, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Property_FocusScales() + { + Assert.Fail("Not implemented"); + } + + [Test] + public void Property_InterpolationColors() + { + var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) }; + var blend = new ColorBlend() + { + Colors = new Color[] { Color.DarkGreen, Color.Aqua, Color.Blue }, + Positions = new float[] { 0.0f, 0.4f, 1.0f } + }; + + using var brush = new PathGradientBrush(points) { InterpolationColors = blend }; + Assert.Multiple(() => + { + var bounds = Utils.GetBoundingRectangle(points); + var center = Utils.GetCenterPoint(points); + + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp)); + Assert.That(brush.Transform.IsIdentity, Is.True); + Assert.That(brush.FocusScales, Is.EqualTo(PointF.Empty)); + Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White })); + + Assert.That(brush.CenterColor, Is.EqualTo(Color.White)); + Assert.That(brush.CenterPoint, Is.EqualTo(center)); + + Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f })); + Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f })); + + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(blend.Colors)); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(blend.Positions)); + + string filepath = Path.Combine("brush", "path", $"TriangleInterpolated.png"); + float similarity = Utils.CompareImage(filepath, brush, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Property_SurroundedColors() + { + var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) }; + var surrounded = new Color[] { Color.Green, Color.Yellow, Color.Blue }; + + using var brush = new PathGradientBrush(points) { SurroundColors = surrounded }; + Assert.Multiple(() => + { + var bounds = Utils.GetBoundingRectangle(points); + var center = Utils.GetCenterPoint(points); + + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp)); + Assert.That(brush.Transform.IsIdentity, Is.True); + Assert.That(brush.FocusScales, Is.EqualTo(PointF.Empty)); + Assert.That(brush.SurroundColors, Is.EqualTo(surrounded)); + + Assert.That(brush.CenterColor, Is.EqualTo(Color.Black)); + Assert.That(brush.CenterPoint, Is.EqualTo(center)); + + Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f })); + Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f })); + + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty })); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f })); + + string filepath = Path.Combine("brush", "path", $"TriangleSurrounded.png"); + float similarity = Utils.CompareImage(filepath, brush, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Property_Transform() + { + Assert.Fail("Not implemented"); + } + + [Test] + public void Method_Clone() + { + var rect = new RectangleF(15, 15, 20, 20); + + using var path = new GraphicsPath(); + path.AddEllipse(rect); + + using var brush1 = new PathGradientBrush(path) { WrapMode = WrapMode.Tile }; + using var brush2 = brush1.Clone() as PathGradientBrush; + Assert.Multiple(() => + { + Assert.That(brush2, Is.Not.Null); + Assert.That(brush2, Is.Not.SameAs(brush1)); + Assert.That(brush2.Transform.Elements, Is.EqualTo(brush1.Transform.Elements)); + Assert.That(brush2.Rectangle, Is.EqualTo(brush1.Rectangle)); + Assert.That(brush2.WrapMode, Is.EqualTo(brush1.WrapMode)); + }); + } +} \ No newline at end of file diff --git a/test/Common/res/images/brush/path/Circle.png b/test/Common/res/images/brush/path/Circle.png new file mode 100644 index 0000000000000000000000000000000000000000..87a7fb8376e9ee32829a080a276a12f14059a612 GIT binary patch literal 1600 zcmV-G2EX}Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1=UGJK~!i%?bvUG zRDBc&@O{^--XxJ;M3O|3ND@gRNhC=mi6oIEk|dHul1LIsB1s}iB#9)EB#|VNM3P7n z$$92;e{*K#K(^jA3hAGYw4MB2%0zEq*??30uI>s z?<3y6jd=4W;`Qs2uU?h(*?rI7y({CBlyw^GHJ2W{sO(WE034t%Uq-xm5%KI<$)`^v zo;)dkrF+kxmvP7;hP-vG*I2i?#pp%|)l)?TR2G2*5A@^55f2|mJa`as|NhW)&og5X zLkb~H|J;6>$@OWK?Bmm^uvk|9HN1Qqpaq?uui4!G#mYx}dlsV>F0~v)uCY4dV zUK{{bUpP%jjYQhXhDfl8%2FT+0DbIO#E~NrhYv>_I#kkU_l_QoIDWhkH3xZXSj(D} zNt4C|vM+34qqGwx009Uv1b_v6*RF^iJ0iAkA57QoBZVl^)S+Z5W9@tQ zit>t=s&x7dpz4jf8Wz?Uw{Mq|C+$RuBmp3xKyTR+v3YaErcDtWHR=?n0V6 z$S8<4t!MTo|!n zLCHmnB9<8>_iD9 zfSx};V$Ph1S+gQ$&yJWow-7=GSvK3cwWz5ePCZI{zofkskt-ZJ9TTbd2dTY@ZKBCF zs4A+as%m6vu+x$yg#_@^r$L-hflG9_Z-#FA5|M$DX9NK%D*dPN}+p)kv4pCpk^8ov6b zhDg3Yl1RPLRgDdjNH#elY^CBVPmM{FA|^~IIeBs+H*a21Er}=uWgQ%KBH8fl-O0)I zS12OJtw#{)X^*sfYUMlRk3F^iwD*W!?dUz?&F+iZzrESnYc${Ndb681yBN~&!@p~> z*EoFF5>0sRM`f?y+t@P92)oqNbwJjtZZ0H~|OB2FGarQ=lQwP(f{YmT{kBJD&8R(AY1 zq6wjQQT0nIlQvU50P2KP*YT;+5@4Cvo*9z_%%!m6AfZ@D^rA0*7gdRn$)2%ss*{9( zLyGNyt*3jQ=`l7!fC{WcX+^SrBbr-|Zd3<=(@gac0t6unB`H89a{1YP;Eh3mst8!e zdcADycB5srIw7Et0uV6?u*_@EC~3TT)V0oNTB)6A5#np0fYYoJ*v!0_;1$5UZr<3V y71h@owo76X;QZ`<6a1LPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D28KyQK~!i%?bl(1 zm0J`B;Qpq6=}(eK`Vr|zB#9)EB$6bOM3P7nNfJpSNhFCRktC5Ml0=e7l1LIsB1t5P zWZil8o^@wC_snhP%uM&XcQ(s8XMf+f_xrB*U2A`5!uS{;<70gMzejxg7V-IW#K(^j zA3j99e_ucS{`9F{`}#HF$B*cLeMRLx&;`9;}~-4@Vq58gb%8#HmveXU|4lyclui zN_~z>2vLZlESKu2v%b;4<>-%MB^$B{P{)r)?B5@;Ygfef?Gf9y)z2L}>fGMF5eE)b zQtm|vaSC0#R@Jdqs`K(?O_IhiR#q!YAR&{$$U4v)Hbksh6R~Pl#Ol=%YuDDf&6_K+ zBt$6+Q7_eDH+3`^nXyo&EN(+2r~v4S6%h*;M$Dfdv0y>OqD2wQmPM>wSqW|27_oJ0 z#Lk^VBQuE2`XP*bltNS!Hl_p;OO`~;niVm1YQ*&E5wmAU%$-{axrXH5k?Cf$wq{1V z0hGGXofIgavlEa=^j(gL6C);19w0MsUR7!7(rUML>moL78nD^EeFHUf`gHAVI-YiY z=T1$Svqv`@&3Y*k%T!v5{{f{05|buXB7V=D88K&0wc6svm9YBp%$6;+3GCTZ8$dUU zb*hpU8>uQK6BeX~O=T1?Kq-KLm@*||+O)qCS-!kBfc5LE9jUo{cTGsAD%x^4RlF^D zYZ!?tWdYeCy>kK(c2NgxgbXE+m@#9ZPFf;^*-Nb*JJ!8c?%%JkH+lMxv{p(sbQ2k~ zF(5LmI{^$h6@>59P$OCJsYoZ0;k4tCcCgaJWhBoH8&gCl(qe%H8hC+Nl0a0ktC6(U zaENrXSFc51q*Nak_U=Rx#j0?f6EdD>NJf%KHQ1`#0m)h|5$k&=`&FIv@}^ueKw1k^ zL~#NE5OA_B>uMnb0dq(ot1+;Zvn0EgV$T|B6&Z}kNb96zK*3Q&jo6eWWEC)EWE>&D zGyFE6Bw95birQ~>EydnitCPrR6nuG5MsyL`@)oQAQ^iS*44?q31K@oMBwG^hZ`EKc zU9f$T%o_QXBEu>8@}@=+)>761jK8d)FCs{G@X?YAxVo~@;C zTSR7TKzbX?WRd|8P#Fih4fDvdQ%fRiBh^S-Wqq`NxHzKiT0If*ki*D=vScHhD$XRs z??3{JR2xaaJuQjWQmqG%_&f;#3v`Cx<`Hm@ds`)t zNJ~jlt461-;K={-y}USPG?C%G7BJ5-6Fxe&;6-_H3lQmYrFl`DD^@a zHdBbH4lqCg2V_Qq&Tu^yNCFaK9qVOHB#F_ei@`=o5i&MRbz~EuB$dQ6a!#m&Q~-fg zL2{a!F#5L~+fgzmb2bM=Mw3($Ya{0qy!$=ZsxaDxJ=9UG))JCcz>-*Jm{0JjfD=1a z7)vV+W&BA9Af$i;vRk;8;63wiAkb|TQosS3G00000NkvXXu0mjfdY%>u literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/path/TriangleBlended.png b/test/Common/res/images/brush/path/TriangleBlended.png new file mode 100644 index 0000000000000000000000000000000000000000..b702e980f59209e898c185cbc06bc3461bc02f42 GIT binary patch literal 358 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;BQYC$B+!?x989DHaSSNBzi4m_W$OP#2fP9 zteS^LqtK2UEDKqLxEJ(YbbX%SQYY4ue|FEwBRNc){9Cpiwbv;sZQsRq%4m=6ySs`v zq}9$he!ltc&fi}GYs8DK7~2p3EB-#CDWQ3W1zX?ox6k%HZ%&wO;K{V)<*(xZyV;t~ z9*WU(nD+Dh-v8VzpAU(!7VPaiZ@ZnfN%v5Q=mnDlZ@>L1eQqm|BYNf@$2N&%#)D#s zYZPXf9}{Y-OkAUQBd5oeWp?Yvu7t^k(*$#bj#)MJCOJ85aP-mO_$DeW)busU$$106 wPY1`hHcrul9hI_EgS<4CPJs~r!$s~gt}X5T#n2zopr0BYBc!lvI6;>1s;*b3=DjSL74G){)!Z!;7?B%$B+!?x94*On;b-1AO1Z(p@GBApiwZ# zfkjxNkwZm+iMPXnNm(ZL%bx_7M%IVxito*|-e6pF=lDbaqJ8h)l`-Ek+gJJKu3`lL zWfOV%+{rC8LL)L@)lDe37#BG=Uzh8i5cFWF~2AlZ$KjqgQHbl<& zENL;PbpxWsM6V^#jD!)Fr703}ibB|-2L^9(+vBWhB z5eCPEnphLpDBj5Fab=m^y0I%^vf(tr9HC=YO}$A;4jb0bh!A+Atfs~yedI`6LjDX1 t!8Prjq6a%FWv2#tX)c`tA^uM!lvI6;>1s;*b3=DjSL74G){)!Z!;5Sbf$B+!?x92zV9x@PVdAOaC)m!1&f+Wte z1q;$xxU{OY)E2ldN@3>W5?ipyq^@H!6W76w!k=XB>d*C9GU11CWY_DX^IjgUdXi#& zHoxlK_O2O!@0`p`F0a}b#G-XLhR-kW`n%=NIh+<~ow;ji`M>bjUJVwmgED{1bbapM z=3u&daLpev&ddL|H#r#2n0a3%;n$sb0S#8;Z_>YbrEK(_IVC0Rw=6!PC{xWt~$(69CFmg3JH_ literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/path/TriangleSurrounded.png b/test/Common/res/images/brush/path/TriangleSurrounded.png new file mode 100644 index 0000000000000000000000000000000000000000..fe089858227747e5662e8bdfab9a44503d01f086 GIT binary patch literal 1873 zcmZWqX;f2p7Jo}#5)uiKu!G@^jer4^Nf7}-{t;q=3g;n+fD2H=5VlI$S`g&DxD?!Y zZcyANjZh+sEg|*{)1nqpp4dS`K}TAwhec6J-541d#ECugZSFbu-gED{>+i$8l|;1C zn&-lUAjmpW6|vc(W$&5Gwn%YUMXN=@`J0vNpv%2(Pb~&3T^=Qepw=q>d@5$Kt#Va~ z`4A+y@t)!C-3B!Tu^&f9$m2BOQ-j5u?iadsznlpR`(v;izVNY8&y zQh9Tti0KBBBi4;jh*YW?iPU?Hou-xI+Q*4!P8R=Ys;zI%vmeaP=^Y>1el}~*jOoqW zhL&B$KfEdXc)t6ePhRfnfRCr?t9db5^@f%W16PwwuY+ThS2V;?L;gw4goF7-u$jrx zUtXfWQB-;Bn(rBta^UST&0{#`?sxk3=ar8;R-P%}ZhCP_W9I1V(-k!rL@n8NJIZ2A zbH<6UsuYu#D}VpnilN-_f?uP|;Zb_!HM`KlZPUN4)*t<~a_544dZV}7+uzr|B= zpK6BNv9SR9U-*nRg!_u|)O&&pvzw;pea#z_D|Wu+_1!1Wz-2ZQ0-XF5Ua0g-O5uDI zyBs>rkva~jiCB3QXA6`WU})D5KiQYWZL}M(HE*raN;x+hZzQfdu;r)3Rg;B;t31cy z{7&!SEN{b{HI=z~E5%HvPToo{b?dboIICRZsx`?r zhHOG4Z_RXTI`##e`IUy<*9zJW1N+0hk4n?g^EM{CiOs#AVts?4iMlsfoDm+r4POg_ zk*B<|`f|C`_yN=kL!_cpu5o$kxb7p+4is7*#~zmQ;`|Li=ofAPcuHWp%O!5>`Xe~U zY?mPZ0?pDsf1NYvZq*3-LUgl$PgPf!WTe%v7P{k0r~=rQTjxmA?<)Ly3gJo?V!_JU zGPe`4f;g`0lpoOMX?e(?C~i0LC;injIcyCLp&GwFKiy(*krH4oclD8%=Ep}1Ld7fp zapyIA1nqKEIpQ!VL9F0@8}9Sj$U84s5f zBI7HcjS8onyoTz()97MNTffn8nOu5Bjbe1PgEDbO_+9QQh!z7awI6ndBre?NpEb*d z!3m1o@aXfKWFB>hWiOE0of5?fy*OTBL=h1{#1jtYfOvCJz=9Gk2=sYYP=aRIbU9Xp z(GVU=vQR737M8d$1pX475Q|dB-axlmh|~jPa`9HcMxD75MCC-M;2q?PJDNR3B4Fo55`k(d?T=sltFY56$VRn-ehF?t&X~lW;o_fwgGeP? z263|xI|F93v`X4xWj?z9TuM|L630*SqfO@9LNF8FnNeJNDI^d;bd z)It)z#qEz*faF8l{t33 zQ@fP%L5)t#I{Y>HBfGOMcxCa$mtAc15z6|`^f2uicl-1Ilmg*aco`BVE^maU!vV&^*bxta0nuj-C_;aV% z_X6fGxGUL=ZpPG@@Sd@?`ic`%Dy~7*Jh~LmCw*8(Crd>sK1pSlMsqLrvGFxjHfF@Q zHpaH-EESy`szBe4aK_8%XVn%>pZ@%tRwf1Qm zkXnwJ5oTP$x9}y7@#_Ql5p6Ua#2(`cQvy=rQyd&lX5wexci=N(oI|Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0eMM8K~!i%?bvON z!axv!;r=BkKmrm_fCL;M0R>1v0t%3T10s9Z#5<0zpNX)k~O9^LjF z2&#ga8$rbw4G1d2XhBdmMiYXvFxn85jM0doB#c%BMPoE0C<>z;K|zcK1dU>>Am}Z| z5`w%KYY2M5SVYhOV--PQo}2CJck}`MV)zMIo4?=o0K=I^>x51oUJQR|b5H7j(I|#> z(^ONsw28v7Jg+^qSIS8kt^#wX^@Ud!hMNtcnff}u2=jk~t^tCIF&Yq5gwcYaY>XxZ zWnr`-C>f&>K}i^`2#UsNMo<(+JA#533kVv;SV7QRj3oqlG1d_Dg0YC80mdqVz&tk_ a)pcKV=mC7OPQ*h10000 Date: Mon, 26 Aug 2024 16:52:00 -0300 Subject: [PATCH 135/217] update README with new components --- README.md | 91 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 9247c0d..9d35a1e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ This repository is organized as follows: Root ├─ src : source code │ ├─ Common +│ │ ├─ Drawing2D : elements for GeneXus.Drawing.Drawing2D +│ │ │ └─ ... │ │ ├─ Imaging : elements for GeneXus.Drawing.Imaging │ │ │ └─ ... │ │ ├─ Interop : elements for GeneXus.Drawing.Interop @@ -32,10 +34,12 @@ Root │ │ │ └─ ... │ │ └─ ... : elements for GeneXus.Drawing │ │ -│ └─ PackageREADME.md +│ └─ PackageREADME.md : package readme file │ ├─ test : unit tests │ ├─ Common +│ │ ├─ Drawing2D : unit-tests for GeneXus.Drawing.Drawing2D +│ │ │ └─ ... │ │ ├─ Text : unit-tests for GeneXus.Drawing.Text │ │ │ └─ ... │ │ └─ ... : unit-tests for GeneXus.Drawing @@ -61,28 +65,67 @@ This section describes each module (namespace) specifiying which elements are pa ### GeneXus.Drawing.Common Basic graphics funcionalities based on `System.Drawing`. -| Name | Type | Description -|----------------------|----------|-------------- -| `Bitmap` | Class | Represents an image defined by pixels. -| `Color` | Class | Defines colors used for drawing. -| `Font` | Class | Defines a format for text, including font family, size, and style. -| `Icon` | Class | Represents an icon image. -| `Image` | Class | Represents an image in a specific format. -| `Svg` (1) | Class | Represents Scalable Vector Graphics. -| `Point` | Struct | Defines an x and y coordinate in a 2D plane. -| `PointF` | Struct | Defines a floating-point x and y coordinate in a 2D plane. -| `Rectangle` | Struct | Defines an x, y, width, and height of a rectangle. -| `RectangleF` | Struct | Defines a floating-point x, y, width, and height of a rectangle. -| `Size` | Struct | Defines the width and height of a rectangular area. -| `SizeF` | Struct | Defines the width and height of a rectangular area with floating-point values. -| `FontSlant` | Enum | Specifies the slant of a font. -| `FontStyle` | Enum | Specifies the style of a font. -| `GraphicsUnit` | Enum | Specifies the unit of measure for drawing operations. -| `KnownColor` | Enum | Defines predefined colors. -| `RotateFlipType` | Enum | Specifies how an image is rotated or flipped. +| Name | Type | Description +|-------------------------|----------|-------------- +| `Bitmap` | Class | Represents an image defined by pixels. +| `Brush` | Class | Abstract class for brushes used to fill graphics shapes. +| `Color` | Class | Defines colors used for drawing. +| `Font` | Class | Defines a format for text, including font family, size, and style. +| `Graphics` | Class | Provides methods for drawing on a drawing surface. +| `Icon` | Class | Represents an icon image. +| `Image` | Class | Represents an image in a specific format. +| `Pen` | Class | Defines an object used to draw lines and curves. +| `Region` | Class | Defines the area of a drawing surface. +| `Svg` (1) | Class | Represents Scalable Vector Graphics. +| `TextureBrush` | Class | Defines a brush that uses an image to fill shapes. +| `Point` | Struct | Defines an x and y coordinate in a 2D plane. +| `PointF` | Struct | Defines a floating-point x and y coordinate in a 2D plane. +| `Rectangle` | Struct | Defines an x, y, width, and height of a rectangle. +| `RectangleF` | Struct | Defines a floating-point x, y, width, and height of a rectangle. +| `Size` | Struct | Defines the width and height of a rectangular area. +| `SizeF` | Struct | Defines the width and height of a rectangular area with floating-point values. +| `CopyPixelOperation` | Enum | Specifies the type of pixel copying operation. +| `FontSlant` | Enum | Specifies the slant of a font. +| `FontStyle` | Enum | Specifies the style of a font. +| `GraphicsUnit` | Enum | Specifies the unit of measure for drawing operations. +| `KnownColor` | Enum | Defines predefined colors. +| `RotateFlipType` | Enum | Specifies how an image is rotated or flipped. +| `StringAlignment` | Enum | Specifies the alignment of text within a string. +| `StringDigitSubstitute` | Enum | Specifies how digits are substituted in a string. +| `StringFormatFlags` | Enum | Specifies formatting options for strings. +| `StringTrimming` | Enum | Specifies how text is trimmed when it does not fit. (1) New element (does not belogs to `System.Drawing` library). +### GeneXus.Drawing.Drawing2D +Advanced 2D graphics functionalities based on `System.Drawing.Drawing2D` for complex vector graphics and rendering tasks. + +| Name | Type | Description +|-----------------------|----------|-------------- +| `Blend` | Class | Defines a blend of colors along a gradient. +| `ColorBlend` | Class | Defines a blend of colors for a gradient. +| `GraphicsPath` | Class | Represents a series of connected lines and curves. +| `HatchBrush` | Class | Defines a brush with a hatching pattern. +| `PathGradientBrush` | Class | Defines a brush that fills an area with a gradient of colors. +| `LinearGradientBrush` | Class | Defines a brush that fills an area with a linear gradient of colors. +| `Matrix` | Struct | Defines a transformation matrix for graphics operations. +| `PathData` | Struct | Contains data associated with a GraphicsPath object. +| `CombineMode` | Enum | Specifies how two graphics objects are combined. +| `CompositingQuality` | Enum | Specifies the quality of compositing operations. +| `CoordinateSpace` | Enum | Specifies the coordinate space for transformations. +| `DashCap` | Enum | Specifies the cap style for dashed lines. +| `FillMode` | Enum | Specifies the fill mode for filling shapes. +| `InterpolationMode` | Enum | Specifies the interpolation mode for scaling and resizing. +| `LineCap` | Enum | Specifies the shape of the end of a line. +| `LineJoin` | Enum | Specifies the shape used to join two connected lines. +| `MatrixOrder` | Enum | Specifies the order of matrix transformations. +| `PenAlignment` | Enum | Specifies the alignment of a pen's stroke. +| `PenType` | Enum | Specifies the type of pen used for drawing. +| `PixelOffsetMode` | Enum | Specifies how to offset pixels when drawing. +| `SmoothingMode` | Enum | Specifies the level of smoothing applied to graphics. +| `WarpMode` | Enum | Specifies how text is warped. +| `WrapMode` | Enum | Specifies how text wraps within its container. + ### GeneXus.Drawing.Imaging Advanced image processing based on `System.Drawing.Imaging` to support sophisticated image manipulation and format handling. @@ -100,9 +143,11 @@ Advanced typographic features based on `System.Drawing.Text` for managing and re | Name | Type | Description |---------------------------|----------|-------------- | `FontCollection` | Class | Represents a collection of fonts. -| `InstalledFontCollection` | Class | Represents a collection of installed fonts. -| `PrivateFontCollection` | Class | Represents a collection of private fonts. -| `GenericFontFamilies` | Enum | Specifies generic font families. +| `InstalledFontCollection` | Class | Represents a collection of installed fonts. +| `PrivateFontCollection` | Class | Represents a collection of private fonts. +| `GenericFontFamilies` | Enum | Specifies generic font families. +| `HotkeyPrefix` | Enum | Specifies how hotkey prefixes are rendered. +| `TextRenderingHint` | Enum | Specifies the level of text rendering quality. ### GeneXus.Drawing.Interop Advanced interoperability utilities based on `System.Drawing.Interop` that includes definitions used in font management and graphics rendering. From e73fe8668c847427557d8ae59ec2ecd187b7f998 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 27 Aug 2024 15:24:05 -0300 Subject: [PATCH 136/217] remove unnecesary whitespaces in PathGradientBrush class --- src/Common/Drawing2D/PathGradientBrush.cs | 32 +++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index d26fb16..3b28a69 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -38,7 +38,7 @@ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform) Array.Copy(new[] { 0f }, m_colors.Positions, 1); m_center = new( - points.Average(pt => pt.X), + points.Average(pt => pt.X), points.Average(pt => pt.Y)); m_focus = new(0, 0); @@ -105,12 +105,12 @@ public override object Clone() #region Properties /// - /// Gets or sets a that specifies positions and factors that define a + /// Gets or sets a that specifies positions and factors that define a /// custom falloff for the gradient. /// public Blend Blend { - get => m_factors; + get => m_factors; set => UpdateShader(() => m_factors = value ?? throw new ArgumentNullException(nameof(value))); } @@ -118,7 +118,7 @@ public Blend Blend /// Gets or sets the at the center of the path gradient. /// public Color CenterColor - { + { get => m_color ?? (m_surround.Length > 1 ? Color.Black : Color.White); set => UpdateShader(() => m_color = value); } @@ -127,7 +127,7 @@ public Color CenterColor /// Gets or sets the center of the path gradient. /// public PointF CenterPoint - { + { get => m_center; set => UpdateShader(() => m_center = value); } @@ -135,8 +135,8 @@ public PointF CenterPoint /// /// Gets or sets the focus for the gradient falloff. /// - public PointF FocusScales - { + public PointF FocusScales + { get => m_focus; set => UpdateShader(() => m_focus = value); } @@ -164,13 +164,13 @@ public ColorBlend InterpolationColors public RectangleF Rectangle => m_path.GetBounds(); /// - /// Gets or sets an array of structures that correspond to the + /// Gets or sets an array of structures that correspond to the /// points in the path this gradient fills. /// - public Color[] SurroundColors + public Color[] SurroundColors { get => m_surround; - set => UpdateShader(() => + set => UpdateShader(() => { if (value.Length > m_path.PointCount) throw new ArgumentException("parameter is not valid.", nameof(value)); @@ -179,7 +179,7 @@ public Color[] SurroundColors } /// - /// Gets or sets a copy of the that defines a local geometric + /// Gets or sets a copy of the that defines a local geometric /// transform for this gradient. /// public Matrix Transform @@ -208,8 +208,8 @@ public WrapMode WrapMode #region Methods /// - /// Multiplies the object that represents the local - /// geometric transformation of this object + /// Multiplies the object that represents the local + /// geometric transformation of this object /// by the specified object in the specified order. /// public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend) @@ -222,14 +222,14 @@ public void ResetTransform() => UpdateShader(() => Transform.Reset()); /// - /// Rotates the local geometric transformation of this object + /// Rotates the local geometric transformation of this object /// by the specified amount in the specified order. /// public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend) => UpdateShader(() => Transform.Rotate(angle, order)); /// - /// Scales the local geometric transformation of this object + /// Scales the local geometric transformation of this object /// by the specified amounts in the specified order. /// public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend) @@ -242,7 +242,7 @@ public void SetBlendTriangularShape(float focus, float scale = 1.0f) => UpdateShader(() => m_factors = GetBlendTriangularShape(focus, scale)); /// - /// Creates a gradient brush that changes color starting from the center of the path outward + /// Creates a gradient brush that changes color starting from the center of the path outward /// to the path's boundary. The transition from one color to another is based on a bell-shaped curve. /// public void SetSigmaBellShape(float focus, float scale = 1.0f) From 3b96de83aaa684594cb8eb96bc057267a6312fb9 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 30 Aug 2024 16:16:12 -0300 Subject: [PATCH 137/217] remove ApplyFactor method and fix how colors are calculated based on blend factor and interpolation --- src/Common/Brush.cs | 7 ---- src/Common/Drawing2D/LinearGradientBrush.cs | 37 +++++++++++++++------ src/Common/Drawing2D/PathGradientBrush.cs | 17 ++++++++-- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/Common/Brush.cs b/src/Common/Brush.cs index c87f8f9..d936b99 100644 --- a/src/Common/Brush.cs +++ b/src/Common/Brush.cs @@ -57,13 +57,6 @@ public void Dispose() #region Utilities - protected static Color ApplyFactor(Color color, float factor) - => Color.FromArgb( - (int)(color.A * factor), - (int)(color.R * factor), - (int)(color.G * factor), - (int)(color.B * factor)); - protected static Drawing2D.Blend GetBlendTriangularShape(float focus, float scale) { if (focus < 0 || focus > 1) diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index 8da930c..ca746a3 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -367,23 +367,29 @@ private void UpdateShader(Action action) var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat; var blend = new Dictionary(); - for (int index = 0; index < m_factors.Positions.Length && m_factors.Positions.Length > 1; index++) + if (m_factors.Positions.Length > 1) { - var pos = m_factors.Positions[index]; - var fac = m_factors.Factors[index]; - blend[pos] = fac; // blend factor + for (int index = 0; index < m_factors.Positions.Length; index++) + { + var pos = m_factors.Positions[index]; + var fac = m_factors.Factors[index]; + blend[pos] = fac; // blend factor + } } - for (int index = 0; index < m_colors.Positions.Length && m_colors.Positions.Length > 1; index++) + if (m_colors.Positions.Length > 1) { - var pos = m_colors.Positions[index]; - var col = m_colors.Colors[index]; - blend[pos] = col; // specific color + for (int index = 0; index < m_colors.Positions.Length; index++) + { + var pos = m_colors.Positions[index]; + var col = m_colors.Colors[index]; + blend[pos] = col; // specific color + } } var positions = blend.Keys.OrderBy(key => key).ToArray(); var colors = new SKColor[positions.Length]; - var lastColor = Color.Empty; + var lastColor = m_colors.Colors[0]; for (int index = 0; index < positions.Length; index++) { var key = positions[index]; @@ -397,7 +403,18 @@ private void UpdateShader(Action action) } if (value is float factor) { - var color = ApplyFactor(lastColor, factor); + var nextColor = m_colors.Colors[m_colors.Colors.Length - 1]; + for (int i = index + 1; i < positions.Length; i++) + { + key = positions[i]; + value = blend[key]; + if (value is Color foundColor) + { + nextColor = foundColor; + break; + } + } + var color = Color.Blend(lastColor, nextColor, factor); colors[index] = color.m_color; continue; } diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 3b28a69..7c7d0e4 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -440,8 +440,8 @@ private void UpdateShader(Action action) var positions = blend.Keys.OrderBy(key => key).ToArray(); var colors = new Color[positions.Length]; - - var lastColor = Color.Empty; + + var lastColor = m_surround[0]; for (int index = 0; index < positions.Length; index++) { var key = positions[index]; @@ -454,7 +454,18 @@ private void UpdateShader(Action action) } if (value is float factor) { - var color = ApplyFactor(lastColor, factor); + var nextColor = m_color ?? Color.White; + for (int i = index + 1; i < positions.Length; i++) + { + key = positions[i]; + value = blend[key]; + if (value is Color foundColor) + { + nextColor = foundColor; + break; + } + } + var color = Color.Blend(lastColor, nextColor, factor); colors[index] = color; continue; } From b2aba42168b1cc5b15400f714e7ed5e131edbf4e Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 30 Aug 2024 16:19:22 -0300 Subject: [PATCH 138/217] fix PathGradientBrush.UpdateShader when just using blend factors --- src/Common/Drawing2D/PathGradientBrush.cs | 82 +++++++++++++------ .../Drawing2D/PathGradientBrushUnitTest.cs | 2 +- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 7c7d0e4..8d87053 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -270,13 +270,53 @@ private static GraphicsPath CreatePath(PointF[] points) return new GraphicsPath(points, types); } - private Color ComputeColor(int x, int y, Vector2[] points, PathPointType[] types, Color[] colors, float[] positions) + public static Vector2 Intersect(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) + { + Vector2 d1 = p1 - p0; // line p0-p1 + Vector2 d2 = p3 - p2; // line p2-p3 + + float det = d1.X * d2.Y - d1.Y * d2.X; + if (Math.Abs(det) < 1e-6) // check if lines are parallel + return new Vector2(float.MaxValue, float.MaxValue); + + float t1 = ((p2.X - p0.X) * d2.Y - (p2.Y - p0.Y) * d2.X) / det; + return p0 + t1 * d1; + } + + public static bool Triangulated(Vector2 pt, Vector2 p0, Vector2 p1, Vector2 p2) + { + bool EdgeCheck(Vector2 a, Vector2 b) + => (a.Y <= pt.Y && pt.Y < b.Y || b.Y <= pt.Y && pt.Y < a.Y) + && (pt.X + 1e-5 < (b.X - a.X) * (pt.Y - a.Y) / (b.Y - a.Y) + a.X); + + return EdgeCheck(p0, p2) ^ EdgeCheck(p1, p0) ^ EdgeCheck(p2, p1); + } + + private static Vector2 Project(Vector2 pt, Vector2 p0, Vector2 p1) + { + var e0 = p1 - p0; + var e1 = pt - p0; + + // point projection in the line p0-p1 + float t = Vector2.Dot(e0, e1) / Vector2.Dot(e0, e0); + return p0 + e0 * t; + } + + private static Vector2 Bezier(Vector2 p0, Vector2 p1, Vector2 p2, float percent) + { + var c0 = Vector2.Lerp(p0, p1, percent); + var c1 = Vector2.Lerp(p1, p2, percent); + + // point in the bezier curve p0-p1-p2 at percent + return Vector2.Lerp(c0, c1, percent); + } + + private Color ComputeColor(int x, int y, Vector2 center, Vector2[] points, PathPointType[] types, Color[] colors, float[] positions) { - var center = m_center.ToVector2(); var color = Color.Transparent; if (m_path.IsVisible(x, y)) { - var coord = new Vector2(x, y); + var target = new Vector2(x, y); if (m_surround.Length > 1) { // determine the weights of this point to the corners of the path @@ -286,12 +326,10 @@ private Color ComputeColor(int x, int y, Vector2[] points, PathPointType[] types var p0 = points[(i + 0) % points.Length]; var p1 = points[(i + 1) % points.Length]; - var e0 = p1 - p0; - var e1 = coord - p0; - var ep = p0 + e0 * Vector2.Dot(e0, e1) / Vector2.Dot(e0, e0); + var ep = Project(target, p0, p1); int j = (i + points.Length - 1) % points.Length; - weight[j] = Vector2.Distance(coord, ep); + weight[j] = Vector2.Distance(target, ep); } float total = weight.Sum(); // for normalizing in 0..1 @@ -322,28 +360,22 @@ private Color ComputeColor(int x, int y, Vector2[] points, PathPointType[] types switch(type) { case PathPointType.Line: - var e0 = p1 - p0; - var e1 = coord - p0; - var ep = p0 + e0 * Vector2.Dot(e0, e1) / Vector2.Dot(e0, e0); - - float distEp = Vector2.Distance(coord, ep); - float dmaxEp = Vector2.Distance(center, ep); - - if (distEp < dist) + var ep = Intersect(target, center, p0, p1); + if (Triangulated(target, p0, p1, center)) { - dist = distEp; - dmax = dmaxEp; + dist = Vector2.Distance(target, ep); + dmax = Vector2.Distance(center, ep); + + i = points.Length; // break the loop } break; case PathPointType.Bezier: for (float t = 0; t <= 1; t += 0.1f) { - var c0 = Vector2.Lerp(p0, p1, t); - var c1 = Vector2.Lerp(p1, p2, t); - var cp = Vector2.Lerp(c0, c1, t); + var cp = Bezier(p0, p1, p2, t); - var distCp = Vector2.Distance(coord, cp); + var distCp = Vector2.Distance(target, cp); var dmaxCp = Vector2.Distance(center, cp); if (distCp < dist) @@ -360,13 +392,14 @@ private Color ComputeColor(int x, int y, Vector2[] points, PathPointType[] types } } - dist /= dmax; // normalized in 0..1 + // normalized in 0..1 + dist /= dmax; // determine the blended color for the given point by positions color = colors[colors.Length - 1]; for (int i = 0; i < positions.Length - 1; i++) { - if (dist >= positions[i] && dist <= positions[i + 1]) + if (dist >= positions[i] && dist < positions[i + 1]) { float amount = (dist - positions[i]) / (positions[i + 1] - positions[i]); color = Color.Blend(colors[i], colors[i + 1], amount); @@ -396,6 +429,7 @@ private void UpdateShader(Action action) } var bounds = m_path.GetBounds(); + var center = m_center.ToVector2(); var points = m_path.PathPoints .Select(point => point.ToVector2()) .ToArray(); @@ -477,7 +511,7 @@ private void UpdateShader(Action action) { for (int y = 0; y < bitmap.Height; y++) { - var color = ComputeColor(x, y, points, types, colors, positions); + var color = ComputeColor(x, y, center, points, types, colors, positions); bitmap.SetPixel(x, y, color); } } diff --git a/test/Common/Drawing2D/PathGradientBrushUnitTest.cs b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs index f1a5c8c..945b070 100644 --- a/test/Common/Drawing2D/PathGradientBrushUnitTest.cs +++ b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs @@ -93,7 +93,7 @@ public void Property_Blend() Positions = new float[] { 0.0f, 0.4f, 1.0f } }; - using var brush = new PathGradientBrush(points) { Blend = blend, CenterColor = color }; + using var brush = new PathGradientBrush(points) { CenterColor = color, Blend = blend }; Assert.Multiple(() => { var bounds = Utils.GetBoundingRectangle(points); From 39e70d0f1f2d80b140675ce2c010a6156fdb6a05 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 30 Aug 2024 16:20:04 -0300 Subject: [PATCH 139/217] add basic test case for PathGradientBrush.Transform property --- .../Drawing2D/PathGradientBrushUnitTest.cs | 35 +++++++++++++++++- .../res/images/brush/path/CircleTransform.png | Bin 0 -> 1342 bytes 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 test/Common/res/images/brush/path/CircleTransform.png diff --git a/test/Common/Drawing2D/PathGradientBrushUnitTest.cs b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs index 945b070..ca22278 100644 --- a/test/Common/Drawing2D/PathGradientBrushUnitTest.cs +++ b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs @@ -235,7 +235,40 @@ public void Property_SurroundedColors() [Test] public void Property_Transform() { - Assert.Fail("Not implemented"); + var rect = new RectangleF(5, 5, 40, 40); + var color = Color.Red; + + using var path = new GraphicsPath(); + path.AddEllipse(rect); + + var transform = new Matrix(); + transform.Scale(0.75f, 1f); + + using var brush = new PathGradientBrush(path) { CenterColor = color, Transform = transform }; + Assert.Multiple(() => + { + var bounds = path.GetBounds(); + var center = new PointF(rect.X + rect.Width / 2, rect.Y + rect.Height / 2); + + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp)); + Assert.That(brush.Transform, Is.EqualTo(transform)); + Assert.That(brush.FocusScales, Is.EqualTo(new PointF(0, 0))); + Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White })); + + Assert.That(brush.CenterColor, Is.EqualTo(color)); + Assert.That(brush.CenterPoint, Is.EqualTo(center)); + + Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f })); + Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f })); + + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty })); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f })); + + string filepath = Path.Combine("brush", "path", $"CircleTransform.png"); + float similarity = Utils.CompareImage(filepath, brush, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); } [Test] diff --git a/test/Common/res/images/brush/path/CircleTransform.png b/test/Common/res/images/brush/path/CircleTransform.png new file mode 100644 index 0000000000000000000000000000000000000000..1ebc169d0e1ff82e02a02f7e30ba3eb0db992b96 GIT binary patch literal 1342 zcmV-E1;P4>P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1k*`GK~!i%?buPI zRa+DW;6u;7q?d^F5{X145{dK@(Muwch(schNJJu$NJJu$NF*YWNF*YWNF)-GNF)+} z`MBrxs%+!o!q*W+`L(@ z+`M)B><7Mq83xUYCLG>C$nt{+$=$m(@EbRhYuA#iSCcDO>Xl!=Ue^J#-|re2H0xRa zi0-ZE!l5`W2$>a>3IHJB%a@aj7n2JYlJn>5m0!A4f!Sw2y0yw^6um0&#oI97CIg^||ot!#Vul(%U3J6fu0mkp$M*|oX#U`tPQaaG1N0m+ugdhbg z_!B3RW5<%CN9&cF14xw`7)hWR6h*N?RHbO6RRg~Xmx$2`2vrcQ;13^84jxPn9H@Ti zP@Ow|yaH`tXvTd|tW~9Jt(1qDtjV4%qK2&EzDk@tnH)J%0qonC?Aep--d%m~-emv& z3P^QS26#zupH>x#>aradTFI8I$sVD~CQ8Y!EUN?%g5I?&*|8(pwynCcIRJrC85NWS z)O0^uLEB!!`p3?^P0~t-Y1(Mj5RkwG2*l2v$=0pOmMzJqP1TJ7vIY#Ash}ii)wF`H zXxmFz|Ja$g(yg@IskB0iRt=Ow9a#rL@EbNH>(*5_HfIgh=my15l2&NVeI?3+wyT%Q z$!?|mXy9P9(rO7@HE3e{_GI(s8u*$u$*NV!%9Y9L)yewxb_#u$P{O*< zNoga1G=1nY4sNuPSs8BC*toHRSg|5mx-?m`Bw4nsf&s8q1$gu#dNg%kiR!8i|0Hw= zI#?!Ul*rKv|7k(yRRMs73zG#4lEsTFFf_9VXy9q9>4fgwe;_~zvI)HbK@n8ZnNUZt zf)@O|dCAUyy6pCd-#6ixyQ7 zbLJ%T=hq4~AV8}kS~ei9b4geyK)R@iy#uevpfv(WCmQ*$bS?JNbHwY32idQlBPug0 z&k;88Cwo`lCI9-aC7Uz#uI0X+ziVm1Qw87XHQn1U3jOZeZ_`It9c!vN;E%sYj8P8= zl%kDce2=h6di}{4qVdRlA$p$t?l-%DWgaMQJSyMptaPtd6`cN^e^IzGkia$&C^d|K zQP^ZvX8ZqItY}5{Pz)vL1sF8LdD+%dffBeYH*Z}9?e{w%P)c&T*2X_sq1bf){b~YS z;B9Q)x_zw@Kn#*nf47ppsbB#{6RwBZ78WAoPSqp=2}>vsDqD|!J&FoF^sKsuU? zbAh+9dFzBtW9T$&5Y0+uXhu*33y?s8YQWl9 Date: Mon, 2 Sep 2024 00:27:30 -0300 Subject: [PATCH 140/217] define PathGradientBrush helper methods as private: Intersect and Triangulated --- src/Common/Drawing2D/PathGradientBrush.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 8d87053..3f7d0ac 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -270,7 +270,7 @@ private static GraphicsPath CreatePath(PointF[] points) return new GraphicsPath(points, types); } - public static Vector2 Intersect(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) + private static Vector2 Intersect(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) { Vector2 d1 = p1 - p0; // line p0-p1 Vector2 d2 = p3 - p2; // line p2-p3 @@ -283,7 +283,7 @@ public static Vector2 Intersect(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) return p0 + t1 * d1; } - public static bool Triangulated(Vector2 pt, Vector2 p0, Vector2 p1, Vector2 p2) + private static bool Triangulated(Vector2 pt, Vector2 p0, Vector2 p1, Vector2 p2) { bool EdgeCheck(Vector2 a, Vector2 b) => (a.Y <= pt.Y && pt.Y < b.Y || b.Y <= pt.Y && pt.Y < a.Y) From 3ff375d4c74e223d8932ed636f237d4f21600fce Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 2 Sep 2024 00:27:59 -0300 Subject: [PATCH 141/217] apply clamp in PathGradientBrush helper method: Project --- src/Common/Drawing2D/PathGradientBrush.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 3f7d0ac..87f53bd 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -299,7 +299,7 @@ private static Vector2 Project(Vector2 pt, Vector2 p0, Vector2 p1) // point projection in the line p0-p1 float t = Vector2.Dot(e0, e1) / Vector2.Dot(e0, e0); - return p0 + e0 * t; + return p0 + e0 * Math.Min(1, Math.Max(0, t)); } private static Vector2 Bezier(Vector2 p0, Vector2 p1, Vector2 p2, float percent) From 2859456ef71211227d5a76f6a9766f398db1b1e3 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 2 Sep 2024 14:22:38 -0300 Subject: [PATCH 142/217] add support for PathGradientBrush.FocusScale property --- src/Common/Drawing2D/PathGradientBrush.cs | 48 +++++++++++++++-------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 87f53bd..3a89bf6 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -311,7 +311,7 @@ private static Vector2 Bezier(Vector2 p0, Vector2 p1, Vector2 p2, float percent) return Vector2.Lerp(c0, c1, percent); } - private Color ComputeColor(int x, int y, Vector2 center, Vector2[] points, PathPointType[] types, Color[] colors, float[] positions) + private Color ComputeColor(int x, int y, Vector2 center, Vector2[] outer, Vector2[] inner, PathPointType[] types, Color[] colors, float[] positions) { var color = Color.Transparent; if (m_path.IsVisible(x, y)) @@ -320,15 +320,15 @@ private Color ComputeColor(int x, int y, Vector2 center, Vector2[] points, PathP if (m_surround.Length > 1) { // determine the weights of this point to the corners of the path - var weight = new float[points.Length]; - for (int i = 0; i < points.Length; i++) + var weight = new float[outer.Length]; + for (int i = 0; i < outer.Length; i++) { - var p0 = points[(i + 0) % points.Length]; - var p1 = points[(i + 1) % points.Length]; + var p0 = outer[(i + 0) % outer.Length]; + var p1 = outer[(i + 1) % outer.Length]; var ep = Project(target, p0, p1); - int j = (i + points.Length - 1) % points.Length; + int j = (i + outer.Length - 1) % outer.Length; weight[j] = Vector2.Distance(target, ep); } @@ -350,11 +350,15 @@ private Color ComputeColor(int x, int y, Vector2 center, Vector2[] points, PathP // determine the distance of this point to the edge of the path float dist = float.MaxValue, dmax = 0f; - for (int i = 0; i < points.Length; i++) + for (int i = 0; i < outer.Length; i++) { - var p0 = points[(i + 0) % points.Length]; - var p1 = points[(i + 1) % points.Length]; - var p2 = points[(i + 2) % points.Length]; + var p0 = outer[(i + 0) % outer.Length]; + var p1 = outer[(i + 1) % outer.Length]; + var p2 = outer[(i + 2) % outer.Length]; + + var c0 = inner[(i + 0) % inner.Length]; + var c1 = inner[(i + 1) % inner.Length]; + var c2 = inner[(i + 2) % inner.Length]; var type = types[i % types.Length]; switch(type) @@ -363,20 +367,24 @@ private Color ComputeColor(int x, int y, Vector2 center, Vector2[] points, PathP var ep = Intersect(target, center, p0, p1); if (Triangulated(target, p0, p1, center)) { + var ip = Intersect(target, center, c0, c1); + var pp = Project(ip, c0, c1); + dist = Vector2.Distance(target, ep); - dmax = Vector2.Distance(center, ep); + dmax = Vector2.Distance(pp, ep); - i = points.Length; // break the loop + i = outer.Length; // break the loop } break; case PathPointType.Bezier: for (float t = 0; t <= 1; t += 0.1f) { - var cp = Bezier(p0, p1, p2, t); + var bp = Bezier(p0, p1, p2, t); + var cp = Bezier(c0, c1, c2, t); - var distCp = Vector2.Distance(target, cp); - var dmaxCp = Vector2.Distance(center, cp); + var distCp = Vector2.Distance(target, bp); + var dmaxCp = Vector2.Distance(cp, bp); if (distCp < dist) { @@ -430,7 +438,8 @@ private void UpdateShader(Action action) var bounds = m_path.GetBounds(); var center = m_center.ToVector2(); - var points = m_path.PathPoints + + var outer = m_path.PathPoints .Select(point => point.ToVector2()) .ToArray(); var types = m_path.PathTypes @@ -438,6 +447,11 @@ private void UpdateShader(Action action) .Skip(1) // skip PathPointType.Start .ToArray(); + var focus = Matrix3x2.CreateScale(m_focus.X, m_focus.Y); + var inner = outer + .Select(point => Vector2.Transform(point - center, focus) + center) + .ToArray(); + var blend = new Dictionary() { [0] = m_surround[0], [1] = m_color }; if (m_surround.Length > 1) { @@ -511,7 +525,7 @@ private void UpdateShader(Action action) { for (int y = 0; y < bitmap.Height; y++) { - var color = ComputeColor(x, y, center, points, types, colors, positions); + var color = ComputeColor(x, y, center, outer, inner, types, colors, positions); bitmap.SetPixel(x, y, color); } } From bca0277a8e9cdc2c5cc0daafdef85f8c1eb2df83 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 3 Sep 2024 09:58:32 -0300 Subject: [PATCH 143/217] add comments on PathGradientBrush helper methods --- src/Common/Drawing2D/PathGradientBrush.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 3a89bf6..631afa1 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -279,8 +279,9 @@ private static Vector2 Intersect(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) if (Math.Abs(det) < 1e-6) // check if lines are parallel return new Vector2(float.MaxValue, float.MaxValue); - float t1 = ((p2.X - p0.X) * d2.Y - (p2.Y - p0.Y) * d2.X) / det; - return p0 + t1 * d1; + // point the in the intersection + float t = ((p2.X - p0.X) * d2.Y - (p2.Y - p0.Y) * d2.X) / det; + return p0 + t * d1; } private static bool Triangulated(Vector2 pt, Vector2 p0, Vector2 p1, Vector2 p2) @@ -289,6 +290,7 @@ bool EdgeCheck(Vector2 a, Vector2 b) => (a.Y <= pt.Y && pt.Y < b.Y || b.Y <= pt.Y && pt.Y < a.Y) && (pt.X + 1e-5 < (b.X - a.X) * (pt.Y - a.Y) / (b.Y - a.Y) + a.X); + // check if point is in the triangle by checking the edges return EdgeCheck(p0, p2) ^ EdgeCheck(p1, p0) ^ EdgeCheck(p2, p1); } From e1010dd61e0a163ffe5ba24791a6dee1691febda Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 3 Sep 2024 14:47:14 -0300 Subject: [PATCH 144/217] fix PathGradientBrush when ScaleFactor is zero (default value) and improve performance by reducing calculations --- src/Common/Drawing2D/PathGradientBrush.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index 631afa1..f535e85 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -270,14 +270,14 @@ private static GraphicsPath CreatePath(PointF[] points) return new GraphicsPath(points, types); } - private static Vector2 Intersect(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) + private static Vector2? Intersect(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3) { Vector2 d1 = p1 - p0; // line p0-p1 Vector2 d2 = p3 - p2; // line p2-p3 float det = d1.X * d2.Y - d1.Y * d2.X; if (Math.Abs(det) < 1e-6) // check if lines are parallel - return new Vector2(float.MaxValue, float.MaxValue); + return null; // point the in the intersection float t = ((p2.X - p0.X) * d2.Y - (p2.Y - p0.Y) * d2.X) / det; @@ -299,9 +299,13 @@ private static Vector2 Project(Vector2 pt, Vector2 p0, Vector2 p1) var e0 = p1 - p0; var e1 = pt - p0; + // determine the numerator and denominator for scalar factor + float num = Vector2.Dot(e0, e1); + float den = Vector2.Dot(e0, e0) + float.Epsilon; + float t = Math.Min(1, Math.Max(0, num / den)); + // point projection in the line p0-p1 - float t = Vector2.Dot(e0, e1) / Vector2.Dot(e0, e0); - return p0 + e0 * Math.Min(1, Math.Max(0, t)); + return p0 + e0 * t; } private static Vector2 Bezier(Vector2 p0, Vector2 p1, Vector2 p2, float percent) @@ -366,14 +370,16 @@ private Color ComputeColor(int x, int y, Vector2 center, Vector2[] outer, Vector switch(type) { case PathPointType.Line: - var ep = Intersect(target, center, p0, p1); if (Triangulated(target, p0, p1, center)) { - var ip = Intersect(target, center, c0, c1); - var pp = Project(ip, c0, c1); + var ep = Intersect(target, center, p0, p1) ?? throw new Exception("line defined by target and center is parallel to shape edge"); + var ip = Intersect(target, center, c0, c1) ?? center; + + ep = Project(ep, p0, p1); + ip = Project(ip, c0, c1); dist = Vector2.Distance(target, ep); - dmax = Vector2.Distance(pp, ep); + dmax = Vector2.Distance(ip, ep); i = outer.Length; // break the loop } From 97d11dfd2eda10cde23d42017e6b67636d97a86e Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 3 Sep 2024 14:48:26 -0300 Subject: [PATCH 145/217] add FocusScales test-cases for PathGradientBrush --- .../Drawing2D/PathGradientBrushUnitTest.cs | 69 +++++++++++++++++- .../res/images/brush/path/CircleRefocused.png | Bin 0 -> 1561 bytes .../images/brush/path/TriangleRefocused.png | Bin 0 -> 853 bytes 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 test/Common/res/images/brush/path/CircleRefocused.png create mode 100644 test/Common/res/images/brush/path/TriangleRefocused.png diff --git a/test/Common/Drawing2D/PathGradientBrushUnitTest.cs b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs index ca22278..42ecb1c 100644 --- a/test/Common/Drawing2D/PathGradientBrushUnitTest.cs +++ b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs @@ -157,9 +157,74 @@ public void Property_CenterPoint() } [Test] - public void Property_FocusScales() + public void Property_FocusScales_Circular() { - Assert.Fail("Not implemented"); + var rect = new RectangleF(5, 5, 40, 40); + var color = Color.Red; + var focus = new PointF(0.50f, 0.25f); + + using var path = new GraphicsPath(); + path.AddEllipse(rect); + + using var brush = new PathGradientBrush(path) { CenterColor = color, FocusScales = focus }; + Assert.Multiple(() => + { + var bounds = path.GetBounds(); + var center = new PointF(rect.X + rect.Width / 2, rect.Y + rect.Height / 2); + + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp)); + Assert.That(brush.Transform.IsIdentity, Is.True); + Assert.That(brush.FocusScales, Is.EqualTo(focus)); + Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White })); + + Assert.That(brush.CenterColor, Is.EqualTo(color)); + Assert.That(brush.CenterPoint, Is.EqualTo(center)); + + Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f })); + Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f })); + + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty })); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f })); + + string filepath = Path.Combine("brush", "path", $"CircleRefocused.png"); + float similarity = Utils.CompareImage(filepath, brush, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Property_FocusScales_Triangular() + { + var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) }; + var color = Color.Red; + var focus = new PointF(0.50f, 0.25f); + + using var brush = new PathGradientBrush(points) { CenterColor = color, FocusScales = focus }; + Assert.Multiple(() => + { + var bounds = Utils.GetBoundingRectangle(points); + var center = Utils.GetCenterPoint(points); + + Assert.That(brush.Rectangle, Is.EqualTo(bounds)); + Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp)); + Assert.That(brush.Transform.IsIdentity, Is.True); + Assert.That(brush.FocusScales, Is.EqualTo(focus)); + Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White })); + + Assert.That(brush.CenterColor, Is.EqualTo(color)); + Assert.That(brush.CenterPoint, Is.EqualTo(center)); + + Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f })); + Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f })); + + Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty })); + Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f })); + + string filepath = Path.Combine("brush", "path", $"TriangleRefocused.png"); + float similarity = Utils.CompareImage(filepath, brush, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); } [Test] diff --git a/test/Common/res/images/brush/path/CircleRefocused.png b/test/Common/res/images/brush/path/CircleRefocused.png new file mode 100644 index 0000000000000000000000000000000000000000..4bcd2c1eb80cd14806bdd7477dce65f4f28ebd5f GIT binary patch literal 1561 zcmV+!2Il#RP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1+Ga%K~!i%?b&^# zRa+DV@FbE%l1LIs5=kOSB#9)EB+(y z=T7n4w~O0${NBCf!Gl5&F{BW3zs7x=Qw*$yiQc<+-2?yv4%nMFlk3-$t5=I(xsqJI zTz%dQCNn9iAOH~Xz@9ploH&sjJC+empsOGC6&^5JsvdM3%CnevoNc6GCUKO1DUnlF7)ztxA##qFVM@TkFPIlCrpf!TmgwVcKg-uQ}frQA(g2`(FfC0ICcd~P5 zvSUZGZCmlJTZ`BG&h6S&#v*m-P$48k0;7hIkr}~j1hEO__pPc6?NLoA%9?~MkO&Y6 z0JeR5vSmxMX;ZRsW3pjGvVMKCZe4NPjyY$Hv975Jk)Ob*p_&W@Xe{m4OC>3KRoLW& zgn|{p0|yESU^Z_qAlI%V?D_c$&~!vnm_b3LQ!jy0Hn6PzNGOGNQk9X|ix(0X%0;GHX^cb7nGQM)B#> zySJU2Jv*5(b5Hxv>RQl39U7&R$2wF&61QG>onw4lOPol1`0rc zr%p{KPfjLIOeRd|?T$^FRL)PER>;nsTL_UJxw$u)X(AemAj(QZ9ZhKcw@Owzl^aMe z&61QBN&-lj7y!`)3b^s(ld)ry(W8@5qmq#$Ed<4FbH~SwNyd#U#1KbFHp$JsbvF$_ z@ym)@QLC-W>qn96R62+*CEX-N&!m8aEKHeF2moJ$?yn<;xGa$!GO}zA5|b5@_Vhqr ztrmT?io1|L$x38INH(OOu4O_N0O<>`RY!>QkReqKKvopKAd;7Lr6Knw#Qs_Jcu?1e zM+j>M-Rl3cA#&u@2@D{?6}iOEdb z7fib@FzQ7YG?$vB2CZNBY^C6b*VUQMXPq&Z;A!~^ zUcELNP!neV?2pI@8Z7^L-?%4n(+E>Rh^KRw%5+nFLFcr^xNt8 z5pEy&!F(TKRaSoI4G}=HBbT1;m~(^qhRE7_XZMcY_nn1;G&{nMa}%b@5MDg^0|w zfVpkQoi|P$xNfKk{GI(E#YGu8fc@I%P4I(O2g5dO!}h1xh!KAQWJ__&Efh0900000 LNkvXXu0mjfN`Kbw literal 0 HcmV?d00001 diff --git a/test/Common/res/images/brush/path/TriangleRefocused.png b/test/Common/res/images/brush/path/TriangleRefocused.png new file mode 100644 index 0000000000000000000000000000000000000000..0a31976025c8d920ce5853e0bdbe8b7204f9774b GIT binary patch literal 853 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!X^2Bue@E{-7?&TnU&ch3zJIlewEbCR0pJf8d9ZvFMuxA)$R z-&TA5UUu#EU-Q1{Fa6>?f$8D;*|X>5F63wrIP-SxmaS8*byt{c*>?taOQwmf+z@c;{QqBWa-TpdKCV}iaWdrRzjSfp zmOVPYJ|42N-==>0WbxT<|3Pm1$`kH!pV&)XQfqlO&Ul=hS$fqhI)D9@8yZumFV}yz znAfi2p*sK16TkO^4C(K`bh2~HKE1PNZtTj<-MT(JXwtq7-_0I9Y~+voangC8LVn1x z=L%xQRa&_@H#m)Ka=aJpi(9p3N>|q<@n6f^>T}OLZilI6uUxbxzPV+_+uY2;i)N|u^L2Z7S*={< zrKLNSi)#|o2FY|e`#+t^a&kY66b~ti71!l$*E#<9$&$72zm?tIQ1iBnYvqm`EEkfE z&)L-d5O%k*`H;;ixPXXLIO6Uxfp&X_ZATj@F7GqM5E+41vJUHZ>jeE#v!U*_Wn zmKwp@w21ip(?EL^$& z{zDc|=W4W{bN#M0dS__{9|VB*XCVFconr zep|E87bbu;#&7E^@aT9r@%YhW8&4HC%gz3nBlb|Bd*{Aw$L~E`CcFO2jh^(~9PKR< zagTe~DWM4fhF+Lr literal 0 HcmV?d00001 From 27fca8f4bc49bbb0faf3ed179e64914b72a7c09c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 4 Sep 2024 13:08:48 -0300 Subject: [PATCH 146/217] minor change for improving readability --- src/Common/Drawing2D/PathGradientBrush.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs index f535e85..a1e82f8 100644 --- a/src/Common/Drawing2D/PathGradientBrush.cs +++ b/src/Common/Drawing2D/PathGradientBrush.cs @@ -279,7 +279,7 @@ private static GraphicsPath CreatePath(PointF[] points) if (Math.Abs(det) < 1e-6) // check if lines are parallel return null; - // point the in the intersection + // point intersection of p0-p1 and p2-p3 float t = ((p2.X - p0.X) * d2.Y - (p2.Y - p0.Y) * d2.X) / det; return p0 + t * d1; } @@ -296,8 +296,8 @@ bool EdgeCheck(Vector2 a, Vector2 b) private static Vector2 Project(Vector2 pt, Vector2 p0, Vector2 p1) { - var e0 = p1 - p0; - var e1 = pt - p0; + Vector2 e0 = p1 - p0; + Vector2 e1 = pt - p0; // determine the numerator and denominator for scalar factor float num = Vector2.Dot(e0, e1); @@ -305,13 +305,13 @@ private static Vector2 Project(Vector2 pt, Vector2 p0, Vector2 p1) float t = Math.Min(1, Math.Max(0, num / den)); // point projection in the line p0-p1 - return p0 + e0 * t; + return p0 + t * e0; } private static Vector2 Bezier(Vector2 p0, Vector2 p1, Vector2 p2, float percent) { - var c0 = Vector2.Lerp(p0, p1, percent); - var c1 = Vector2.Lerp(p1, p2, percent); + Vector2 c0 = Vector2.Lerp(p0, p1, percent); + Vector2 c1 = Vector2.Lerp(p1, p2, percent); // point in the bezier curve p0-p1-p2 at percent return Vector2.Lerp(c0, c1, percent); From d44ff7cc593aa5b511039dd7e34fcd25ddc23824 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 4 Sep 2024 16:58:26 -0300 Subject: [PATCH 147/217] implement Graphics' Restore and Save methods, add GraphicsState class --- src/Common/Drawing2D/GraphicsState.cs | 12 ++++++++++++ src/Common/Graphics.cs | 8 ++++---- 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/Common/Drawing2D/GraphicsState.cs diff --git a/src/Common/Drawing2D/GraphicsState.cs b/src/Common/Drawing2D/GraphicsState.cs new file mode 100644 index 0000000..860cf77 --- /dev/null +++ b/src/Common/Drawing2D/GraphicsState.cs @@ -0,0 +1,12 @@ +using SkiaSharp; +using System; + +namespace GeneXus.Drawing.Drawing2D; + +public sealed class GraphicsState : MarshalByRefObject +{ + internal int m_index; + internal SKCanvas m_canvas; + + internal GraphicsState(int state) => m_index = state; +} \ No newline at end of file diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 5d06d24..db99f34 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1594,8 +1594,8 @@ public void ResetTransform() /// Restores the state of this to the state /// represented by a . /// - public void Restore(object state) - => throw new NotImplementedException(); // TODO: implement GraphicsState class + public void Restore(GraphicsState state) + => m_canvas.RestoreToCount(state.m_index); /// /// Applies the specified rotation to the transformation matrix of this . @@ -1612,8 +1612,8 @@ public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend /// Saves the current state of this and /// identifies the saved state with a . /// - public object Save() - => throw new NotImplementedException(); // TODO: implement GraphicsState + public GraphicsState Save() + => new(m_canvas.SaveLayer()); /// /// Applies the specified scaling operation to the transformation matrix of From cd02f2b2d6882108c9a972fc580660194b149392 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 4 Sep 2024 17:02:49 -0300 Subject: [PATCH 148/217] remove unused SKCanvas reference from GraphicsState class --- src/Common/Drawing2D/GraphicsState.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsState.cs b/src/Common/Drawing2D/GraphicsState.cs index 860cf77..f76dd0c 100644 --- a/src/Common/Drawing2D/GraphicsState.cs +++ b/src/Common/Drawing2D/GraphicsState.cs @@ -6,7 +6,6 @@ namespace GeneXus.Drawing.Drawing2D; public sealed class GraphicsState : MarshalByRefObject { internal int m_index; - internal SKCanvas m_canvas; internal GraphicsState(int state) => m_index = state; } \ No newline at end of file From 145196d4c206d98fd9357299c2ca4f2fe49828c6 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 5 Sep 2024 14:11:05 -0300 Subject: [PATCH 149/217] fix several indentation issues with spaces --- src/Common/Brush.cs | 2 +- src/Common/Color.cs | 10 +++++----- src/Common/Drawing2D/LinearGradientBrush.cs | 2 +- src/Common/Font.cs | 4 ++-- src/Common/Graphics.cs | 18 +++++++++--------- src/Common/Icon.cs | 4 ++-- src/Common/Point.cs | 2 +- src/Common/PointF.cs | 2 +- src/Common/Region.cs | 8 ++++---- src/Common/TextureBrush.cs | 6 +++--- 10 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Common/Brush.cs b/src/Common/Brush.cs index d936b99..9d47420 100644 --- a/src/Common/Brush.cs +++ b/src/Common/Brush.cs @@ -111,7 +111,7 @@ protected static Drawing2D.Blend GetSigmaBellShape(float focus, float scale = 1. m_blend.Positions[0] = focus; m_blend.Factors[0] = scale; - SigmaBellBlend(ref m_blend, focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, true); + SigmaBellBlend(ref m_blend, focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, true); m_blend.Positions[count - 1] = 1f; m_blend.Factors[count - 1] = 0f; diff --git a/src/Common/Color.cs b/src/Common/Color.cs index a3e8e4c..c4565dc 100644 --- a/src/Common/Color.cs +++ b/src/Common/Color.cs @@ -273,7 +273,7 @@ public static Color FromKnownColor(KnownColor color) #region Methods /// - /// Gets the 32-bit ARGB value of this structure. + /// Gets the 32-bit ARGB value of this structure. /// public readonly int ToArgb() => (A << 24) | (R << 16) | (G << 8) | B; @@ -305,10 +305,10 @@ public static Color Blend(Color color1, Color color2, float amount) if (amount <= 0) return color1; if (amount >= 1) return color2; int r = (int)(color1.R + (color2.R - color1.R) * amount); - int g = (int)(color1.G + (color2.G - color1.G) * amount); - int b = (int)(color1.B + (color2.B - color1.B) * amount); - int a = (int)(color1.A + (color2.A - color1.A) * amount); - return FromArgb(a, r, g, b); + int g = (int)(color1.G + (color2.G - color1.G) * amount); + int b = (int)(color1.B + (color2.B - color1.B) * amount); + int a = (int)(color1.A + (color2.A - color1.A) * amount); + return FromArgb(a, r, g, b); } #endregion diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs index ca746a3..fcdd01b 100644 --- a/src/Common/Drawing2D/LinearGradientBrush.cs +++ b/src/Common/Drawing2D/LinearGradientBrush.cs @@ -155,7 +155,7 @@ public Color[] LinearColors public RectangleF Rectangle => m_rect; /// - /// Gets or sets a copy of the object + /// Gets or sets a copy of the object /// that defines a local geometric transformation for the image associated /// with this object. /// diff --git a/src/Common/Font.cs b/src/Common/Font.cs index b323db5..8394a99 100644 --- a/src/Common/Font.cs +++ b/src/Common/Font.cs @@ -329,13 +329,13 @@ public float GetHeight(Graphics graphics) => GetHeight(graphics.PageUnit, graphics.DpiY); /// - /// Returns the height, in pixels, of this when drawn to a device with the specified vertical resolution. + /// Returns the height, in pixels, of this when drawn to a device with the specified vertical resolution. /// public float GetHeight(float dpi) => GetHeight(Unit, dpi); /// - /// Returns the height, in pixels, of this when drawn to a device with the specified vertical resolution + /// Returns the height, in pixels, of this when drawn to a device with the specified vertical resolution /// and with the specified . /// private float GetHeight(GraphicsUnit unit, float dpi) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index db99f34..ca7a3c2 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -491,7 +491,7 @@ public void DrawIconUnstretched(Icon icon, Rectangle rect) /// /// Draws the specified , using its original physical size, at the specified - /// location given by a structure. + /// location given by a structure. /// public void DrawImage(Image image, PointF point) => DrawImage(image, point.X, point.Y); @@ -504,7 +504,7 @@ public void DrawImage(Image image, float x, float y) /// /// Draws the specified , using its original physical size, at the specified - /// location given by a structure. + /// location given by a structure. /// public void DrawImage(Image image, Point point) => DrawImage(image, new PointF(point.m_point)); @@ -1440,7 +1440,7 @@ public Region[] MeasureCharacterRanges(string text, Font font, RectangleF layout /// /// Measures the specified string when drawn with the specified in a area, - /// formatted with the specified , and the maximum bounding box + /// formatted with the specified , and the maximum bounding box /// specified by a structure. /// public SizeF MeasureString(string text, Font font, RectangleF layoutArea = default, StringFormat format = null) @@ -1609,7 +1609,7 @@ public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend } /// - /// Saves the current state of this and + /// Saves the current state of this and /// identifies the saved state with a . /// public GraphicsState Save() @@ -1727,12 +1727,12 @@ public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrd private static float GetUnitFactor(float dpi, GraphicsUnit unit) => unit switch { - GraphicsUnit.Pixel => 1f, + GraphicsUnit.Pixel => 1f, GraphicsUnit.Display => dpi / 72f, - GraphicsUnit.Point => dpi / 72f, - GraphicsUnit.Inch => dpi, - GraphicsUnit.Document => dpi / 300f, - GraphicsUnit.Millimeter => dpi / 25.4f, + GraphicsUnit.Point => dpi / 72f, + GraphicsUnit.Inch => dpi, + GraphicsUnit.Document => dpi / 300f, + GraphicsUnit.Millimeter => dpi / 25.4f, _ => throw new ArgumentException($"{unit} unit not supported.", nameof(unit)), }; diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs index 78b371f..0d2631f 100644 --- a/src/Common/Icon.cs +++ b/src/Common/Icon.cs @@ -36,7 +36,7 @@ private Icon(Bitmap bitmap, List entries, float width, float height) } /// - /// Initializes a new instance of the class from a instance. + /// Initializes a new instance of the class from a instance. /// public Icon(Bitmap bitmap) : this(bitmap, new() { new IconEntry { Width = (byte)bitmap.Width, Height = (byte)bitmap.Height } }, -1, -1) { } @@ -256,7 +256,7 @@ private static List ReadIco(Stream stream) { Reserved = reader.ReadUInt16(), Type = reader.ReadUInt16(), // Type (1 for icon, 2 for cursor) - Count = reader.ReadUInt16() // Number of images + Count = reader.ReadUInt16() // Number of images }; // Read ICONDIRENTRY structures diff --git a/src/Common/Point.cs b/src/Common/Point.cs index 980284a..c10d8b9 100644 --- a/src/Common/Point.cs +++ b/src/Common/Point.cs @@ -59,7 +59,7 @@ public Point(int dw) /// /// Compares two objects. The result specifies whether the values of the /// or properties of the two - /// objects are unequal. + /// objects are unequal. /// public static bool operator !=(Point left, Point right) => left.m_point != right.m_point; diff --git a/src/Common/PointF.cs b/src/Common/PointF.cs index b95461f..25b52f2 100644 --- a/src/Common/PointF.cs +++ b/src/Common/PointF.cs @@ -77,7 +77,7 @@ public PointF(Vector2 vector) /// /// Compares two objects. The result specifies whether the values of the /// or properties of the two - /// objects are unequal. + /// objects are unequal. /// public static bool operator !=(PointF left, PointF right) => left.m_point != right.m_point; diff --git a/src/Common/Region.cs b/src/Common/Region.cs index 6ebde67..11c3597 100644 --- a/src/Common/Region.cs +++ b/src/Common/Region.cs @@ -195,9 +195,9 @@ public RegionData GetRegionData() var rdh = new RGNDATAHEADER { - dwSize = (uint)Marshal.SizeOf(), - iType = 1, // RDH_RECTANGLES - nCount = (uint)rects.Count, + dwSize = (uint)Marshal.SizeOf(), + iType = 1, // RDH_RECTANGLES + nCount = (uint)rects.Count, nRgnSize = (uint)(rects.Count * Marshal.SizeOf()) }; var headerSize = Marshal.SizeOf(); @@ -474,7 +474,7 @@ private static SKRegion ParseRegionData(RegionData data) var rdh = new RGNDATAHEADER { - dwSize = BitConverter.ToUInt32(rgnData, 0), + dwSize = BitConverter.ToUInt32(rgnData, 0), iType = BitConverter.ToUInt32(rgnData, 4), nCount = BitConverter.ToUInt32(rgnData, 8), nRgnSize = BitConverter.ToUInt32(rgnData, 12) diff --git a/src/Common/TextureBrush.cs b/src/Common/TextureBrush.cs index 115ebd0..0051372 100644 --- a/src/Common/TextureBrush.cs +++ b/src/Common/TextureBrush.cs @@ -10,7 +10,7 @@ public sealed class TextureBrush : Brush internal RectangleF m_bounds; internal WrapMode m_mode; - private TextureBrush(RectangleF rect, Image image, WrapMode mode) + private TextureBrush(RectangleF rect, Image image, WrapMode mode) : base(new SKPaint { }) { m_bounds = rect; @@ -79,9 +79,9 @@ public override object Clone() public Image Image => m_image; /// - /// Gets or sets a copy of the object + /// Gets or sets a copy of the object /// that defines a local geometric transformation for the image associated - /// with this object. + /// with this object. /// public Matrix Transform { get; set; } = new Matrix(); From 54d50e8fb9d588b00b8609e50efc6abc975a232c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 6 Sep 2024 09:21:45 -0300 Subject: [PATCH 150/217] fix Graphics.Transform property --- src/Common/Graphics.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index ca7a3c2..0c0dabf 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -152,7 +152,11 @@ private void Dispose(bool disposing) /// /// Gets or sets a copy of the geometric world transformation for this . /// - public Matrix Transform { get; set; } = new Matrix(); + public Matrix Transform + { + get => new(m_canvas.TotalMatrix); + set => m_canvas.SetMatrix(value.m_matrix); + } /// /// Gets or sets the world transform elements for this . From b50c7f7d48ad4a37da2fef423aa6dce439a79868 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 6 Sep 2024 09:22:22 -0300 Subject: [PATCH 151/217] fix Graphics.Save method to use SKCanvas.Save method instead of SKCanvas.SaveLayer method --- src/Common/Graphics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 0c0dabf..9dca432 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1617,7 +1617,7 @@ public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend /// identifies the saved state with a . /// public GraphicsState Save() - => new(m_canvas.SaveLayer()); + => new(m_canvas.Save()); /// /// Applies the specified scaling operation to the transformation matrix of From a4ab4c6b2e867e1cb7c9933c47a60b3aee67d932 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 6 Sep 2024 09:24:15 -0300 Subject: [PATCH 152/217] fix Graphics.DrawCurve, Graphics.DrawClosedCurve and Graphics.FillClosedCurve in order to use the same GetCurvePath helper method --- src/Common/Graphics.cs | 45 ++++++++++++------------------------------ 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 9dca432..1be7f3a 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -378,7 +378,7 @@ public void DrawCachedBitmap(object cachedBitmap, int x, int y) /// public void DrawClosedCurve(Pen pen, PointF[] points, float tension = 0.5f, FillMode fillMode = FillMode.Winding) { - using var path = GetClosedCurvePath(points, fillMode, tension); + using var path = GetCurvePath(points, fillMode, tension, true); m_canvas.DrawPath(path.m_path, pen.m_paint); } @@ -394,39 +394,17 @@ public void DrawClosedCurve(Pen pen, Point[] points, float tension = 0.5f, FillM /// public void DrawCurve(Pen pen, PointF[] points, int offset, int numberOfSegments, float tension = 0.5f) { - // TODO: include tension if (offset < 0 || offset >= points.Length) throw new ArgumentOutOfRangeException(nameof(offset)); - if (numberOfSegments < 1 || offset + numberOfSegments >= points.Length) + if (numberOfSegments < 1 || offset + numberOfSegments > points.Length) throw new ArgumentOutOfRangeException(nameof(numberOfSegments)); - using var path = new SKPath(); - path.MoveTo(points[offset].m_point); - for (int i = offset + 1; i < offset + numberOfSegments - 1; i++) - { - PointF p0 = points[Math.Max(i - 1, 0)]; - PointF p1 = points[i + 0]; - PointF p2 = points[i + 1]; - PointF p3 = points[Math.Min(i + 2, points.Length - 1)]; - - // calculate the tangents - float t1x = (p2.X - p0.X) * tension; - float t1y = (p2.Y - p0.Y) * tension; - float t2x = (p3.X - p1.X) * tension; - float t2y = (p3.Y - p1.Y) * tension; - - // calculate the control points - var p1x = points[i + 0].X + t1x / 3; - var p1y = points[i + 0].Y + t1y / 3; - var p2x = points[i + 1].X - t2x / 3; - var p2y = points[i + 1].Y - t2y / 3; - - // add the cubic Bezier segment to the path - path.CubicTo(p1x, p1y, p2x, p2y, points[i + 1].X, points[i + 1].Y); - } + var fillMode = FillMode.Alternate; + points = points.Skip(offset).Take(numberOfSegments).ToArray(); - m_canvas.DrawPath(path, pen.m_paint); + using var path = GetCurvePath(points, fillMode, tension, false); + m_canvas.DrawPath(path.m_path, pen.m_paint); } /// @@ -1140,7 +1118,7 @@ public void ExcludeClip(Rectangle rect) /// public void FillClosedCurve(Brush brush, PointF[] points, FillMode fillMode = FillMode.Alternate, float tension = 0.5f) { - using var path = GetClosedCurvePath(points, fillMode, tension); + using var path = GetCurvePath(points, fillMode, tension, true); m_canvas.DrawPath(path.m_path, brush.m_paint); } @@ -1745,14 +1723,17 @@ public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrd #region Helpers - private static GraphicsPath GetClosedCurvePath(PointF[] points, FillMode fillMode, float tension) + private static GraphicsPath GetCurvePath(PointF[] points, FillMode fillMode, float tension, bool closed) { - // TODO: include tension if (points.Length < 3) throw new ArgumentException("invalid number of points for drawing a closed curve (at least 3)"); var path = new GraphicsPath(fillMode); - path.AddClosedCurve(points); + if (closed) + path.AddClosedCurve(points, tension); + else + path.AddCurve(points, tension); + return path; } From 90ffc2b9d65b69815f24b851d4c3948757704e3b Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 6 Sep 2024 09:29:05 -0300 Subject: [PATCH 153/217] fix mutually infinite recursion in Graphics.IsVisible overloads --- src/Common/Graphics.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 1be7f3a..3c2cf12 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1337,7 +1337,7 @@ public bool IsVisible(PointF point) /// within the visible clip region of this . /// public bool IsVisible(Point point) - => IsVisible(point.X, point.Y); + => IsVisible(new PointF(point.X, point.Y)); /// /// Indicates whether the specified structure is contained @@ -1351,7 +1351,7 @@ public bool IsVisible(RectangleF rect) /// within the visible clip region of this . /// public bool IsVisible(Rectangle rect) - => IsVisible(rect.X, rect.Y, rect.Width, rect.Height); + => IsVisible(new RectangleF(rect.X, rect.Y, rect.Width, rect.Height)); /// /// Indicates whether the point specified by a pair of coordinates structure is contained From 4e8996b0cbfac1274d9069e0f7c83f425b66ce8c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 6 Sep 2024 12:53:03 -0300 Subject: [PATCH 154/217] fix Graphics.DrawImage method over rectangles --- src/Common/Graphics.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 3c2cf12..5baf05a 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -545,13 +545,13 @@ public void DrawImage(Image image, Point[] points) /// Draws a portion of an at a specified location. /// public void DrawImage(Image image, float x, float y, RectangleF rect, GraphicsUnit unit) - => DrawImage(image, rect, new RectangleF(x, y, image.Width, image.Height), unit); + => DrawImage(image, new RectangleF(x, y, rect.Width, rect.Height), rect, unit); /// /// Draws a portion of an at a specified location. /// public void DrawImage(Image image, int x, int y, Rectangle rect, GraphicsUnit unit) - => DrawImage(image, rect, new RectangleF(x, y, image.Width, image.Height), unit); + => DrawImage(image, new RectangleF(x, y, rect.Width, rect.Height), rect, unit); /// /// Draws the specified portion of the specified at the specified location @@ -562,8 +562,8 @@ public void DrawImage(Image image, RectangleF destination, RectangleF source, Gr float factorX = GetUnitFactor(DpiX, unit); float factorY = GetUnitFactor(DpiY, unit); - var src = RectangleF.Inflate(source, factorX, factorY); - var dst = RectangleF.Inflate(destination, factorX, factorY); + var src = new RectangleF(source.X, source.Y, source.Width * factorX, source.Height * factorY); + var dst = new RectangleF(destination.X, destination.Y, destination.Width * factorX, destination.Height * factorY); m_canvas.DrawImage(image.InnerImage, src.m_rect, dst.m_rect); } From 4574ff23abb20cb0b251f3b8c76790d6627c4d4a Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 6 Sep 2024 13:07:02 -0300 Subject: [PATCH 155/217] fix Graphics.DrawImage method over custom shape --- src/Common/Graphics.cs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 5baf05a..b46f080 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -581,22 +581,17 @@ public void DrawImage(Image image, Rectangle destination, Rectangle source, Grap public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object imageAtt = null, object callback = null, int callbackData = 0) { // TODO: Implement ImageAttributes (attributes), DrawImageAbort (callback) and IntPtr (callbackData) - var path = new SKPath(); + using var path = new SKPath(); + path.MoveTo(destPoints[0].m_point); for (int i = 1; i < destPoints.Length; i++) path.LineTo(destPoints[i].m_point); path.Close(); + + m_canvas.ClipPath(path); - float factorX = GetUnitFactor(DpiX, srcUnit); - float factorY = GetUnitFactor(DpiY, srcUnit); - - using var surface = SKSurface.Create(image.InnerImage.Info); - surface.Canvas.DrawImage(image.InnerImage, 0, 0); - surface.Canvas.ClipPath(path); - surface.Canvas.ClipRect(srcRect.m_rect); - surface.Canvas.Scale(factorX, factorY); - - m_canvas.DrawSurface(surface, 0, 0); + var dstRect = new RectangleF(path.Bounds); + DrawImage(image, dstRect, srcRect, srcUnit); } /// From b6c56aecad22e9ba084371234cef6b8f318db844 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 6 Sep 2024 16:46:23 -0300 Subject: [PATCH 156/217] add support for DigitSubstitutionMethod and DigitSubstitutionLanguage in StringFormat class --- src/Common/Drawing2D/GraphicsPath.cs | 1 + src/Common/StringFormat.cs | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index c1c4637..f2c3396 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -943,6 +943,7 @@ private void AddString(string text, FontFamily family, int style, float emSize, text = format.ApplyDirection(text); text = format.ApplyTabStops(text); text = format.ApplyControlEscape(text); + text = format.ApplyDigitSubstitution(text); text = format.ApplyWrapping(text, layout, paint.MeasureText); text = format.ApplyTrimming(text, layout, paint.FontSpacing, paint.MeasureText); text = format.ApplyHotkey(text, out var underlines); diff --git a/src/Common/StringFormat.cs b/src/Common/StringFormat.cs index db2641c..7d65939 100644 --- a/src/Common/StringFormat.cs +++ b/src/Common/StringFormat.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -96,7 +97,7 @@ private void Dispose(bool disposing) { } /// /// Gets the language of for this . /// - public int DigitSubstitutionLanguage { get; private set; } + public int DigitSubstitutionLanguage { get; private set; } = CultureInfo.InvariantCulture.LCID; /// /// Gets or sets a that contains formatting information. @@ -195,7 +196,7 @@ public void SetTabStops(float firstTabOffset, float[] tabStops) .Where(char.IsControl) .ToArray(); - internal static readonly Dictionary CONTROL_CHARACTERS = new Dictionary + internal static readonly Dictionary CONTROL_CHARACTERS = new() { { '\u0000', '␀' }, // Null { '\u0001', '␁' }, // Start of Heading @@ -327,6 +328,25 @@ internal string ApplyTabStops(string text) } + internal string ApplyDigitSubstitution(string text) + { + var sb = new StringBuilder(text); + if (DigitSubstitutionMethod != StringDigitSubstitute.None) + { + var lcid = DigitSubstitutionMethod == StringDigitSubstitute.User + ? CultureInfo.CurrentCulture.LCID + : DigitSubstitutionLanguage; + + var culture = new CultureInfo(lcid); + + var format = culture.NumberFormat; + for (int i = 0; i < format.NativeDigits.Length; i++) + sb.Replace(i.ToString(), format.NativeDigits[i]); + } + return sb.ToString(); + } + + internal string ApplyControlEscape(string text) { var sb = new StringBuilder(text); From a8fe2e015ff15980b34b4b5893040cd7c4ca6e61 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Sun, 8 Sep 2024 13:35:09 -0300 Subject: [PATCH 157/217] fix Graphics' clip methods --- src/Common/Graphics.cs | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index b46f080..50dfe57 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -12,11 +12,13 @@ public sealed class Graphics : IDisposable { internal readonly SKCanvas m_canvas; internal readonly SKBitmap m_bitmap; + private int m_context; private Graphics(SKBitmap bitmap) { m_bitmap = bitmap; m_canvas = new SKCanvas(m_bitmap); + m_context = -1; VisibleClipBounds = new RectangleF(m_canvas.LocalClipBounds); Clip = new Region(VisibleClipBounds); @@ -281,7 +283,10 @@ public void Clear() /// Clears the entire drawing surface and fills it with the specified background color. /// public void Clear(Color color) - => m_canvas.Clear(color.m_color); + { + ClipColor = color; + m_canvas.Clear(ClipColor.m_color); + } /// /// Performs a bit-block transfer of the color data, corresponding to a rectangle of pixels, @@ -1558,7 +1563,16 @@ public void ReleaseHdcInternal(IntPtr hdc) /// Resets the clip region of this to an infinite region. /// public void ResetClip() - => SetClip(Rectangle.Empty); + { + m_context = -1; + + // NOTE: reset without losing the transformation matrix + var matrix = m_canvas.TotalMatrix; + m_canvas.RestoreToCount(m_context); + m_canvas.SetMatrix(matrix); + + m_canvas.Clear(ClipColor.m_color); + } /// /// Resets the world transformation matrix of this to @@ -1636,8 +1650,12 @@ public void SetClip(Region region, CombineMode combineMode = CombineMode.Replace case CombineMode.Xor: Clip.Xor(region); break; - }; - throw new ArgumentException($"{combineMode} value is not supported", nameof(combineMode)); + + default: + throw new ArgumentException($"{combineMode} value is not supported", nameof(combineMode)); + } + m_context = m_canvas.Save(); + m_canvas.ClipRegion(Clip.m_region); } /// @@ -1683,7 +1701,16 @@ public void TransformPoints(CoordinateSpace destination, CoordinateSpace source, /// amounts in the horizontal and vertical directions. /// public void TranslateClip(float dx, float dy) - => Clip.Translate(dx, dy); + { + // NOTE: restore without losing the transformation matrix + var matrix = m_canvas.TotalMatrix; + m_canvas.RestoreToCount(m_context); + m_canvas.SetMatrix(matrix); + + Clip.Translate(dx, dy); + m_canvas.ClipRegion(Clip.m_region); + m_canvas.Clear(ClipColor.m_color); + } /// /// Changes the origin of the coordinate system by applying the specified translation to the @@ -1718,6 +1745,8 @@ public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrd #region Helpers + private Color ClipColor { get; set;} + private static GraphicsPath GetCurvePath(PointF[] points, FillMode fillMode, float tension, bool closed) { if (points.Length < 3) From 8c93e089040a642abf9f5df677f903f31d0e01b3 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Sun, 8 Sep 2024 13:35:56 -0300 Subject: [PATCH 158/217] fix Graphics.VisibleClipBounds definition (getter only) --- src/Common/Graphics.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 50dfe57..9cc9197 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -20,8 +20,7 @@ private Graphics(SKBitmap bitmap) m_canvas = new SKCanvas(m_bitmap); m_context = -1; - VisibleClipBounds = new RectangleF(m_canvas.LocalClipBounds); - Clip = new Region(VisibleClipBounds); + Clip = new Region(ClipBounds); } /// @@ -172,7 +171,7 @@ public Matrix3x2 TransformElements /// /// Gets the bounding rectangle of the visible clipping region of this . /// - public RectangleF VisibleClipBounds { get; set; } + public RectangleF VisibleClipBounds => Clip.GetBounds(this); #endregion From bb5448c3e2692412058f8faacf1cfd6ed8286878 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Sun, 8 Sep 2024 13:36:13 -0300 Subject: [PATCH 159/217] minor change in method description --- src/Common/Graphics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 9cc9197..90ae3eb 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1276,7 +1276,7 @@ public IntPtr GetHdc() => throw new NotSupportedException("skia unsupported feature"); /// - /// Gets the nearest color to the specified structure. + /// Gets the nearest color to the specified structure. /// public Color GetNearestColor(Color color) { From 1057bb74b831f7da1319c4322fe71246e25c9812 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Sun, 8 Sep 2024 13:53:51 -0300 Subject: [PATCH 160/217] fix Grapchis.RotateTransform method angle --- src/Common/Graphics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 90ae3eb..625166e 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1592,7 +1592,7 @@ public void Restore(GraphicsState state) /// public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend) { - var scaleMatrix = SKMatrix.CreateRotationDegrees(angle); + var scaleMatrix = SKMatrix.CreateRotationDegrees(-angle); if (order == MatrixOrder.Append) scaleMatrix = scaleMatrix.PreConcat(m_canvas.TotalMatrix); m_canvas.Concat(ref scaleMatrix); From 123d1f370f7926464caa828ce5015fa816c6c2db Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Sun, 8 Sep 2024 14:06:50 -0300 Subject: [PATCH 161/217] fix Graphics.MultiplyTransform method --- src/Common/Graphics.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 625166e..dd30c78 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1536,7 +1536,11 @@ public SizeF MeasureStringInternal(string text, Font font, RectangleF layoutArea /// speciried the in the specified order. /// public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend) - => m_canvas.SetMatrix(matrix.m_matrix); + { + var result = new Matrix(Transform.m_matrix); + result.Multiply(matrix, order); + Transform = result; + } /// /// Releases a device context handle obtained by a previous call to From d406633cbe9c6cb9fb15c5e79cdae22555f09a05 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 9 Sep 2024 14:33:44 -0300 Subject: [PATCH 162/217] uniformize the way of getting DPI value in Graphics and update Font to use that implementation in order to improve maintainability --- src/Common/Font.cs | 37 +++++-------------------------- src/Common/Graphics.cs | 50 ++++++++++++++++++++++++++++-------------- 2 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/Common/Font.cs b/src/Common/Font.cs index 8394a99..2ea3bbc 100644 --- a/src/Common/Font.cs +++ b/src/Common/Font.cs @@ -288,7 +288,7 @@ void AppendStyle(string value) /// /// The em-size, in points, of this . [Browsable(false)] - public float SizeInPoints => Size * GetFactor(DPI, Unit, GraphicsUnit.Point); + public float SizeInPoints => Size * Graphics.GetFactor(Graphics.DPI.Y, Unit, GraphicsUnit.Point); /// /// Gets the unit of measure for this . @@ -320,7 +320,10 @@ void AppendStyle(string value) /// Returns the line spacing, in pixels, of this . /// public float GetHeight() - => GetHeight(DPI); + { + using var graphics = new Graphics(new SKBitmap(1, 1)) { PageUnit = Unit }; + return GetHeight(graphics); + } /// /// Returns the line spacing, in the current unit of a specified Graphics, of this . @@ -339,7 +342,7 @@ public float GetHeight(float dpi) /// and with the specified . /// private float GetHeight(GraphicsUnit unit, float dpi) - => (Metrics.Descent - Metrics.Ascent + Metrics.Leading) * GetFactor(dpi, unit, GraphicsUnit.Pixel); + => (Metrics.Descent - Metrics.Ascent + Metrics.Leading) * Graphics.GetFactor(dpi, unit, GraphicsUnit.Pixel); /// /// Creates a from the specified handle to a device context (HDC). @@ -456,33 +459,5 @@ public static int GetFontCount(Stream stream) private SKFontMetrics Metrics => Typeface.ToFont(Size).Metrics; - private static int DPI - { - get - { - using var surface = SKSurface.Create(new SKImageInfo(50, 50)); - return (int)(100f * surface.Canvas.DeviceClipBounds.Width / surface.Canvas.LocalClipBounds.Width); - } - } - - private static float GetFactor(float dpi, GraphicsUnit sourceUnit, GraphicsUnit targetUnit) - { - float sourceFactor = GetPointFactor(sourceUnit, dpi); - float targetFactor = GetPointFactor(targetUnit, dpi); - return sourceFactor / targetFactor; - - static float GetPointFactor(GraphicsUnit unit, float dpi) => unit switch - { - GraphicsUnit.World => throw new NotSupportedException("World unit conversion is not supported."), - GraphicsUnit.Display => 72 / dpi, // Assuming display unit is pixels - GraphicsUnit.Pixel => 72 / dpi, // 1 pixel = 72 points per inch / Dots Per Inch - GraphicsUnit.Point => 1, // Already in points - GraphicsUnit.Inch => 72, // 1 inch = 72 points - GraphicsUnit.Document => 72 / 300f, // 1 document unit = 1/300 inch - GraphicsUnit.Millimeter => 72 / 25.4f, // 1 millimeter = 1/25.4 inch - _ => throw new ArgumentException("Invalid GraphicsUnit") - }; - } - #endregion } diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index dd30c78..a5e84ef 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -14,7 +14,16 @@ public sealed class Graphics : IDisposable internal readonly SKBitmap m_bitmap; private int m_context; - private Graphics(SKBitmap bitmap) + internal static readonly (int X, int Y) DPI; + + static Graphics() + { + using var surface = SKSurface.Create(new SKImageInfo(50, 50)); + DPI.X = (int)(100f * surface.Canvas.DeviceClipBounds.Width / surface.Canvas.LocalClipBounds.Width); + DPI.Y = (int)(100f * surface.Canvas.DeviceClipBounds.Height / surface.Canvas.LocalClipBounds.Height); + } + + internal Graphics(SKBitmap bitmap) { m_bitmap = bitmap; m_canvas = new SKCanvas(m_bitmap); @@ -93,12 +102,12 @@ private void Dispose(bool disposing) /// /// Gets the horizontal resolution of this . /// - public float DpiX => (int)(100 * m_canvas.DeviceClipBounds.Width / m_canvas.LocalClipBounds.Width); + public float DpiX => DPI.X; /// /// Gets the vertical resolution of this . /// - public float DpiY => (int)(100 * m_canvas.DeviceClipBounds.Height / m_canvas.LocalClipBounds.Height); + public float DpiY => DPI.Y; /// /// Gets or sets the interpolation mode associated with this . @@ -247,8 +256,8 @@ public GraphicsContainer BeginContainer(RectangleF dstRect, RectangleF srcRect, { int state = m_canvas.Save(); - float factorX = GetUnitFactor(DpiX, unit); - float factorY = GetUnitFactor(DpiY, unit); + float factorX = GetFactor(DpiX, unit, GraphicsUnit.Pixel); + float factorY = GetFactor(DpiY, unit, GraphicsUnit.Pixel); var src = RectangleF.Inflate(srcRect, factorX, factorY); var dst = RectangleF.Inflate(dstRect, factorX, factorY); @@ -563,8 +572,8 @@ public void DrawImage(Image image, int x, int y, Rectangle rect, GraphicsUnit un /// public void DrawImage(Image image, RectangleF destination, RectangleF source, GraphicsUnit unit) { - float factorX = GetUnitFactor(DpiX, unit); - float factorY = GetUnitFactor(DpiY, unit); + float factorX = GetFactor(DpiX, unit, GraphicsUnit.Pixel); + float factorY = GetFactor(DpiY, unit, GraphicsUnit.Pixel); var src = new RectangleF(source.X, source.Y, source.Width * factorX, source.Height * factorY); var dst = new RectangleF(destination.X, destination.Y, destination.Width * factorX, destination.Height * factorY); @@ -1732,16 +1741,25 @@ public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrd #region Utitlies - private static float GetUnitFactor(float dpi, GraphicsUnit unit) => unit switch + internal static float GetFactor(float dpi, GraphicsUnit sourceUnit, GraphicsUnit targetUnit) { - GraphicsUnit.Pixel => 1f, - GraphicsUnit.Display => dpi / 72f, - GraphicsUnit.Point => dpi / 72f, - GraphicsUnit.Inch => dpi, - GraphicsUnit.Document => dpi / 300f, - GraphicsUnit.Millimeter => dpi / 25.4f, - _ => throw new ArgumentException($"{unit} unit not supported.", nameof(unit)), - }; + float sourceFactor = GetPointFactor(sourceUnit, dpi); + float targetFactor = GetPointFactor(targetUnit, dpi); + return sourceFactor / targetFactor; + + static float GetPointFactor(GraphicsUnit unit, float dpi) => unit switch + { + GraphicsUnit.World => throw new NotSupportedException("World unit conversion is not supported."), + GraphicsUnit.Display => 72 / dpi, // Assuming display unit is pixels + GraphicsUnit.Pixel => 72 / dpi, // 1 pixel = 72 points per inch / Dots Per Inch + GraphicsUnit.Point => 1, // Already in points + GraphicsUnit.Inch => 72, // 1 inch = 72 points + GraphicsUnit.Document => 72 / 300f, // 1 document unit = 1/300 inch + GraphicsUnit.Millimeter => 72 / 25.4f, // 1 millimeter = 1/25.4 inch + _ => throw new ArgumentException("Invalid GraphicsUnit") + }; + } + #endregion From d66b2a2e49d9bee621cda0aef2cfa628e812bbfd Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 9 Sep 2024 14:35:40 -0300 Subject: [PATCH 163/217] minor changes --- src/Common/Drawing2D/CoordinateSpace.cs | 2 +- src/Common/Drawing2D/Matrix.cs | 7 ++++--- src/Common/Graphics.cs | 2 +- test/Common/Drawing2D/PathGradientBrushUnitTest.cs | 1 - 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Common/Drawing2D/CoordinateSpace.cs b/src/Common/Drawing2D/CoordinateSpace.cs index c855c0b..9d5b6ad 100644 --- a/src/Common/Drawing2D/CoordinateSpace.cs +++ b/src/Common/Drawing2D/CoordinateSpace.cs @@ -13,7 +13,7 @@ public enum CoordinateSpace /// /// Specifies that coordinates are in the page coordinate context. Their units are defined - /// by the property, and must be one of the elements of + /// by the property, and must be one of the elements of /// the enumeration. /// Page = 1, diff --git a/src/Common/Drawing2D/Matrix.cs b/src/Common/Drawing2D/Matrix.cs index da24e14..606f453 100644 --- a/src/Common/Drawing2D/Matrix.cs +++ b/src/Common/Drawing2D/Matrix.cs @@ -269,7 +269,7 @@ public void TransformPoints(PointF[] points) /// array of . /// public void TransformPoints(Point[] points) - => TransformPoints(points, m_matrix, p => new(p), p => p.m_point); + => TransformPoints(points, Transpose(m_matrix), p => new(p), p => p.m_point); /// /// Applies only the scale and rotate components of this to the specified @@ -350,8 +350,9 @@ private static void TransformPoints(T[] points, SKMatrix matrix, Func Date: Mon, 9 Sep 2024 15:39:30 -0300 Subject: [PATCH 164/217] fix GraphicsPath.AddBeziers method --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index f2c3396..74e2c43 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -800,7 +800,7 @@ private void AddBeziers(params SKPoint[] points) if (points.Length % 4 != 0) throw new ArgumentException($"beziers requires points lenght with multiple of 4", nameof(points)); for (int i = 0; i < points.Length; i += 4) - AddBezier(points[i], points[i + 1], points[i + 2], points[i + 4]); + AddBezier(points[i], points[i + 1], points[i + 2], points[i + 3]); } From 58d74907734a349d5704c23149e79bf86216635d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 9 Sep 2024 15:43:14 -0300 Subject: [PATCH 165/217] change GraphicsPath constructor visibility to internal --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 74e2c43..494ee00 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -9,7 +9,7 @@ public sealed class GraphicsPath : ICloneable, IDisposable { internal readonly SKPath m_path; - private GraphicsPath(SKPath path) + internal GraphicsPath(SKPath path) { m_path = path; } From 174899a1886ce16e23a51ecfb6b676110c5a7c98 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 9 Sep 2024 16:31:49 -0300 Subject: [PATCH 166/217] improve Graphics.DrawImage for maintainability --- src/Common/Graphics.cs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index e7b7e4f..c896c7c 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -572,13 +572,14 @@ public void DrawImage(Image image, int x, int y, Rectangle rect, GraphicsUnit un /// public void DrawImage(Image image, RectangleF destination, RectangleF source, GraphicsUnit unit) { - float factorX = GetFactor(DpiX, unit, GraphicsUnit.Pixel); - float factorY = GetFactor(DpiY, unit, GraphicsUnit.Pixel); - - var src = new RectangleF(source.X, source.Y, source.Width * factorX, source.Height * factorY); - var dst = new RectangleF(destination.X, destination.Y, destination.Width * factorX, destination.Height * factorY); - - m_canvas.DrawImage(image.InnerImage, src.m_rect, dst.m_rect); + var dst = new PointF[] + { + new(destination.Left, destination.Top), + new(destination.Right, destination.Top), + new(destination.Right, destination.Bottom), + new(destination.Left, destination.Bottom) + }; + DrawImage(image, dst, source, unit); } /// @@ -600,11 +601,18 @@ public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, Grap for (int i = 1; i < destPoints.Length; i++) path.LineTo(destPoints[i].m_point); path.Close(); - + m_canvas.ClipPath(path); var dstRect = new RectangleF(path.Bounds); - DrawImage(image, dstRect, srcRect, srcUnit); + + float factorX = GetFactor(DpiX, srcUnit, GraphicsUnit.Pixel); + float factorY = GetFactor(DpiY, srcUnit, GraphicsUnit.Pixel); + + var src = new RectangleF(srcRect.X, srcRect.Y, srcRect.Width * factorX, srcRect.Height * factorY); + var dst = new RectangleF(dstRect.X, dstRect.Y, dstRect.Width * factorX, dstRect.Height * factorY); + + m_canvas.DrawImage(image.InnerImage, src.m_rect, dst.m_rect); } /// From 82be880832e747f26d31c15726a58c99727c80a5 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 9 Sep 2024 16:33:27 -0300 Subject: [PATCH 167/217] fix missin gusing in Graphics.MeasureStringInternal method --- src/Common/Graphics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index c896c7c..29b5a79 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1529,7 +1529,7 @@ public SizeF MeasureStringInternal(ReadOnlySpan text, Font font, Rectangle /// public SizeF MeasureStringInternal(string text, Font font, RectangleF layoutArea, StringFormat format, out int charsFitted, out int linesFilled) { - var path = new GraphicsPath(); + using var path = new GraphicsPath(); path.AddString(text, font.FontFamily, (int)font.Style, font.Size, layoutArea, format); var bounds = path.GetBounds(); From b9acac7fa437e289d5de2239b54c6bee937a12dc Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 9 Sep 2024 16:37:14 -0300 Subject: [PATCH 168/217] fix GraphicsPath.AddLines method --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 494ee00..3328ab3 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -403,7 +403,7 @@ public void AddLine(Point pt1, Point pt2) /// of the current figure defined by a sequence of structures. /// public void AddLines(params PointF[] points) - => Array.ForEach(Enumerable.Range(0, points.Length - 2).ToArray(), i => AddLine(points[i], points[i + 1])); + => Array.ForEach(Enumerable.Range(0, points.Length - 1).ToArray(), i => AddLine(points[i], points[i + 1])); /// /// Appends a series of connected line segments to the end From bb8165ed368dd861b0ae0f7f9e24029be98d1e39 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 9 Sep 2024 20:16:20 -0300 Subject: [PATCH 169/217] implement Graphics.TransformPoints method with CoordinateSpace parameters --- src/Common/Graphics.cs | 64 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 29b5a79..958142a 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1806,12 +1806,66 @@ private static GraphicsPath GetPolygonPath(PointF[] points, FillMode fillMode) private void TransformPoints(CoordinateSpace destination, CoordinateSpace source, T[] points, Func getPoint, Func newPoint) { - // TODO: implement CoordinateSpace - var matrix = m_canvas.TotalMatrix; - for (int i = 0; i < points.Length; i++) + if (source == destination) + return; + + void ApplyTransform(SKMatrix matrix) + { + for (int i = 0; i < points.Length; i++) + { + var srcPoint = getPoint(points[i]); + var dstPoint = matrix.MapPoint(srcPoint); + points[i] = newPoint(dstPoint); + } + } + + // get factors according to page unot and scale + var factorX = GetFactor(DpiX, PageUnit, GraphicsUnit.Pixel) * PageScale; + var factorY = GetFactor(DpiY, PageUnit, GraphicsUnit.Pixel) * PageScale; + + var factorMatrix = SKMatrix.CreateScale(factorX, factorY); + + // get the destination and source matrices + var dstTransMatrix = new SKMatrix(m_canvas.TotalMatrix.Values); + var dstScaleMatrix = SKMatrix.Concat(dstTransMatrix, factorMatrix); + + var srcTransMatrix = dstTransMatrix.Invert(); + var srcScaleMatrix = dstScaleMatrix.Invert(); + + // apply transform based on source + switch (source) + { + case CoordinateSpace.World: + break; + + case CoordinateSpace.Page: + ApplyTransform(srcTransMatrix); + break; + + case CoordinateSpace.Device: + ApplyTransform(srcScaleMatrix); + break; + + default: + throw new ArgumentException($"{source} coordinate space is not supported.", nameof(source)); + } + + // apply transform based on destination + switch (destination) { - var point = matrix.MapPoint(getPoint(points[i])); - points[i] = newPoint(point); + case CoordinateSpace.World: + break; + + case CoordinateSpace.Page: + ApplyTransform(dstTransMatrix); + break; + + case CoordinateSpace.Device: + ApplyTransform(dstScaleMatrix); + break; + + default: + throw new ArgumentException($"{destination} coordinate space is not supported.", nameof(destination)); } } From 8ac2b4899f29367cad4f44481d9181c627e82c98 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 09:26:30 -0300 Subject: [PATCH 170/217] add message to exceptions that not have one --- src/Common/Graphics.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 958142a..b231520 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -308,7 +308,7 @@ public void CopyFromScreen(Point srcUpperLeft, Point dstUpperLeft, Size blockReg /// from the screen to the drawing surface of the . /// public void CopyFromScreen(int srcX, int srcY, int dstX, int dstY, Size blockRegionSize, CopyPixelOperation copyPixelOperation = CopyPixelOperation.SourceCopy) - => throw new NotSupportedException(); + => throw new NotSupportedException("skia unsupported feature"); /// /// Draws an arc representing a portion of an ellipse specified by a structure. @@ -1571,13 +1571,13 @@ public void ReleaseHdc() /// the method of this . /// public void ReleaseHdc(IntPtr hdc) - => throw new NotSupportedException(); + => throw new NotSupportedException("skia unsupported feature"); /// /// Releases a handle to a device context. /// public void ReleaseHdcInternal(IntPtr hdc) - => throw new NotSupportedException(); + => throw new NotSupportedException("skia unsupported feature"); /// /// Resets the clip region of this to an infinite region. From facf33a83dee14d8754ef95449781634e2776064 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 09:27:01 -0300 Subject: [PATCH 171/217] fix Graphics.GetNearestColor method --- src/Common/Graphics.cs | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index b231520..eea2bb3 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1296,29 +1296,7 @@ public IntPtr GetHdc() /// Gets the nearest color to the specified structure. /// public Color GetNearestColor(Color color) - { - var closest = SKColor.Empty; - var pivot = double.MaxValue; - - foreach (var candidate in m_bitmap.Pixels.Distinct()) - { - var powDiffs = new double[] - { - Math.Pow(color.R - candidate.Red, 2), - Math.Pow(color.G - candidate.Green, 2), - Math.Pow(color.B - candidate.Blue, 2), - Math.Pow(color.A - candidate.Alpha, 2) - }; - - var distance = Math.Sqrt(powDiffs.Sum()); - if (distance < pivot) - { - closest = candidate; - pivot = distance; - } - } - return new Color(closest); - } + => color; // NOTE: skia does not provides the device color palette to find the nearest color /// /// Updates the clip region of this to the intersection From 23e5a2b223b06067cd90e869a861a8afb4a8d055 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 11:00:00 -0300 Subject: [PATCH 172/217] fix Graphics.BeginContainer method --- src/Common/Graphics.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index eea2bb3..9ae167d 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -246,7 +246,10 @@ public void AddMetafileComment(byte[] data) /// and opens and uses a new graphics container. /// public GraphicsContainer BeginContainer() - => BeginContainer(new RectangleF(0, 0, 1, 1), new RectangleF(0, 0, 1, 1), GraphicsUnit.Pixel); + { + var rect = new RectangleF(m_canvas.LocalClipBounds); + return BeginContainer(rect, rect, GraphicsUnit.Pixel); + } /// /// Saves a graphics container with the current state of this and @@ -254,13 +257,13 @@ public GraphicsContainer BeginContainer() /// public GraphicsContainer BeginContainer(RectangleF dstRect, RectangleF srcRect, GraphicsUnit unit) { - int state = m_canvas.Save(); + int state = m_canvas.SaveLayer(); float factorX = GetFactor(DpiX, unit, GraphicsUnit.Pixel); float factorY = GetFactor(DpiY, unit, GraphicsUnit.Pixel); - var src = RectangleF.Inflate(srcRect, factorX, factorY); - var dst = RectangleF.Inflate(dstRect, factorX, factorY); + var src = new RectangleF(srcRect.X, srcRect.Y, srcRect.Width * factorX, srcRect.Height * factorY); + var dst = new RectangleF(dstRect.X, dstRect.Y, dstRect.Width * factorX, dstRect.Height * factorY); float scaleX = dst.Width / src.Width; float scaleY = dst.Height / src.Height; From 1c66a1acc1073cc9d6c4b542820a5aef7e43e1ed Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 11:00:25 -0300 Subject: [PATCH 173/217] add Graphics.GetContextInfo missing method --- src/Common/Graphics.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 9ae167d..4f68244 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1283,6 +1283,27 @@ public void Flush(FlushIntention intention = FlushIntention.Flush) m_canvas.Flush(); } + /// + /// Combines current Graphics context with all previous contexts. + /// When BeginContainer() is called, a copy of the current context is pushed into the GDI+ context stack, it keeps track of the + /// absolute clipping and transform but reset the public properties so it looks like a brand new context. + /// When Save() is called, a copy of the current context is also pushed in the GDI+ stack but the public clipping and transform + /// properties are not reset (cumulative). Consecutive Save context are ignored with the exception of the top one which contains + /// all previous information. + /// The return value is an object array where the first element contains the cumulative clip region and the second the cumulative + /// translate transform matrix. + /// + public object GetContextInfo() + { + using var path = new GraphicsPath(m_path); + + using var cumulativeClip = m_canvas.IsClipEmpty ? null : new Region(path); + var cumulativeTransform = TransformElements; + + // TODO: keep the context tracking when calling to BeginContainer/Save and EndContainer/Restore + return new object[] { cumulativeClip ?? new Region(), new Matrix(cumulativeTransform) }; + } + /// /// Gets a handle to the current Windows halftone palette. /// From d0627923403ae583175228558d7707c4b64bdf54 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 15:55:22 -0300 Subject: [PATCH 174/217] fix typo in RegionUnitTest --- test/Common/RegionUnitTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Common/RegionUnitTest.cs b/test/Common/RegionUnitTest.cs index 90227b9..ee92b6c 100644 --- a/test/Common/RegionUnitTest.cs +++ b/test/Common/RegionUnitTest.cs @@ -4,7 +4,7 @@ namespace GeneXus.Drawing.Test; -internal class RecgionUnitTest +internal class RegionUnitTest { private static readonly string IMAGE_PATH = Path.Combine( Directory.GetParent(Environment.CurrentDirectory).Parent.FullName, From d3d50cfe1053093852f4726e2b913fd0514fbdc6 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 15:55:52 -0300 Subject: [PATCH 175/217] change Region constructor visiblity and fix MakeInfinite method --- src/Common/Region.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Region.cs b/src/Common/Region.cs index 11c3597..8bb6e08 100644 --- a/src/Common/Region.cs +++ b/src/Common/Region.cs @@ -10,7 +10,7 @@ public sealed class Region : IDisposable { internal readonly SKRegion m_region; - private Region(SKRegion region) + internal Region(SKRegion region) { m_region = region; } @@ -368,7 +368,7 @@ public void MakeEmpty() /// Initializes this to an empty interior. /// public void MakeInfinite() - => m_region.SetRect(SKRectI.Create(int.MaxValue, int.MaxValue)); + => m_region.SetRect(SKRectI.Create(-4194304, -4194304, 8388608, 8388608)); /// /// Releases the handle of the . From d6440033ad123d1fa3f921ff8fc9be12e58cea06 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 16:05:16 -0300 Subject: [PATCH 176/217] fix Graphics.Clip property --- src/Common/Graphics.cs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 4f68244..080612a 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -82,7 +82,11 @@ private void Dispose(bool disposing) /// /// Gets or sets a that limits the drawing region of this . /// - public Region Clip { get; set; } + public Region Clip + { + get => new Region(ClipRegion.m_region); + set => SetClip(value); + } /// /// Gets a structure that bounds the clipping region of this . @@ -1588,12 +1592,13 @@ public void ResetClip() { m_context = -1; - // NOTE: reset without losing the transformation matrix + // NOTE: apply a full reset without losing the transformation matrix var matrix = m_canvas.TotalMatrix; m_canvas.RestoreToCount(m_context); m_canvas.SetMatrix(matrix); - m_canvas.Clear(ClipColor.m_color); + ClipRegion.MakeInfinite(); + SetClip(ClipRegion); } /// @@ -1650,34 +1655,35 @@ public void SetClip(Region region, CombineMode combineMode = CombineMode.Replace switch (combineMode) { case CombineMode.Replace: - Clip = region; + ClipRegion = region; break; case CombineMode.Union: - Clip.Union(region); + ClipRegion.Union(region); break; case CombineMode.Intersect: - Clip.Intersect(region); + ClipRegion.Intersect(region); break; case CombineMode.Exclude: - Clip.Exclude(region); + ClipRegion.Exclude(region); break; case CombineMode.Complement: - Clip.Complement(region); + ClipRegion.Complement(region); break; case CombineMode.Xor: - Clip.Xor(region); + ClipRegion.Xor(region); break; default: throw new ArgumentException($"{combineMode} value is not supported", nameof(combineMode)); } m_context = m_canvas.Save(); - m_canvas.ClipRegion(Clip.m_region); + m_canvas.ClipRegion(ClipRegion.m_region); + m_canvas.Clear(ClipColor.m_color); } /// @@ -1778,6 +1784,8 @@ internal static float GetFactor(float dpi, GraphicsUnit sourceUnit, GraphicsUnit private Color ClipColor { get; set; } + private Region ClipRegion { get; set; } + private static GraphicsPath GetCurvePath(PointF[] points, FillMode fillMode, float tension, bool closed) { if (points.Length < 3) From 2f4e8482c460b469227293fd87fe40b7e9137f37 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 16:08:13 -0300 Subject: [PATCH 177/217] change every Graphics draw/fill method to use path for implementing IsVisible method --- src/Common/Graphics.cs | 103 ++++++++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 080612a..77f98a6 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -12,6 +12,7 @@ public sealed class Graphics : IDisposable { internal readonly SKCanvas m_canvas; internal readonly SKBitmap m_bitmap; + internal readonly SKPath m_path; // NOTE: tracks every shape added in order to implement IsVisible method private int m_context; internal static readonly (int X, int Y) DPI; @@ -27,6 +28,7 @@ internal Graphics(SKBitmap bitmap) { m_bitmap = bitmap; m_canvas = new SKCanvas(m_bitmap); + m_path = new SKPath(); m_context = -1; Clip = new Region(ClipBounds); @@ -321,7 +323,11 @@ public void CopyFromScreen(int srcX, int srcY, int dstX, int dstY, Size blockReg /// Draws an arc representing a portion of an ellipse specified by a structure. /// public void DrawArc(Pen pen, RectangleF oval, float startAngle, float sweepAngle) - => m_canvas.DrawArc(oval.m_rect, startAngle, sweepAngle, false, pen.m_paint); + { + using var path = new GraphicsPath(); + path.AddArc(oval, startAngle, sweepAngle); + DrawPath(pen, path); + } /// /// Draws an arc representing a portion of an ellipse specified by a pair of coordinates, a width, and a height. @@ -373,12 +379,9 @@ public void DrawBeziers(Pen pen, params PointF[] points) if (points.Length < 4 || points.Length % 3 != 1) throw new ArgumentException("invalid number of points for drawing Bezier curves", nameof(points)); - using var path = new SKPath(); - path.MoveTo(points[0].m_point); - for (int i = 1; i < points.Length - 2; i += 3) - path.CubicTo(points[i].m_point, points[i + 1].m_point, points[i + 2].m_point); - - m_canvas.DrawPath(path, pen.m_paint); + using var path = new GraphicsPath(); + path.AddBeziers(points); + DrawPath(pen, path); } /// @@ -399,7 +402,7 @@ public void DrawCachedBitmap(object cachedBitmap, int x, int y) public void DrawClosedCurve(Pen pen, PointF[] points, float tension = 0.5f, FillMode fillMode = FillMode.Winding) { using var path = GetCurvePath(points, fillMode, tension, true); - m_canvas.DrawPath(path.m_path, pen.m_paint); + DrawPath(pen, path); } /// @@ -424,7 +427,7 @@ public void DrawCurve(Pen pen, PointF[] points, int offset, int numberOfSegments points = points.Skip(offset).Take(numberOfSegments).ToArray(); using var path = GetCurvePath(points, fillMode, tension, false); - m_canvas.DrawPath(path.m_path, pen.m_paint); + DrawPath(pen, path); } /// @@ -457,7 +460,10 @@ public void DrawEllipse(Pen pen, float x, float y, float width, float height) /// Draws an ellipse specified by a bounding structure. /// public void DrawEllipse(Pen pen, RectangleF rect) - => m_canvas.DrawOval(rect.m_rect, pen.m_paint); + { + using var path = GetEllipsePath(rect); + DrawPath(pen, path); + } /// /// Draws an ellipse defined by a bounding rectangle specified by coordinates for the upper-left corner of the @@ -602,16 +608,14 @@ public void DrawImage(Image image, Rectangle destination, Rectangle source, Grap public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object imageAtt = null, object callback = null, int callbackData = 0) { // TODO: Implement ImageAttributes (attributes), DrawImageAbort (callback) and IntPtr (callbackData) - using var path = new SKPath(); - - path.MoveTo(destPoints[0].m_point); - for (int i = 1; i < destPoints.Length; i++) - path.LineTo(destPoints[i].m_point); - path.Close(); + using var path = new GraphicsPath(); + path.AddLines(destPoints); + path.CloseFigure(); - m_canvas.ClipPath(path); + m_path.AddPath(path.m_path); + m_canvas.ClipPath(path.m_path); - var dstRect = new RectangleF(path.Bounds); + var dstRect = path.GetBounds(); float factorX = GetFactor(DpiX, srcUnit, GraphicsUnit.Pixel); float factorY = GetFactor(DpiY, srcUnit, GraphicsUnit.Pixel); @@ -701,7 +705,11 @@ public void DrawLine(Pen pen, float x1, float y1, float x2, float y2) /// Draws a line connecting two structures. /// public void DrawLine(Pen pen, PointF point1, PointF point2) - => m_canvas.DrawLine(point1.m_point, point2.m_point, pen.m_paint); + { + using var path = new GraphicsPath(); + path.AddLine(point1, point2); + DrawPath(pen, path); + } /// /// Draws a line connecting the two points specified by the coordinate pairs. @@ -736,7 +744,7 @@ public void DrawLines(Pen pen, params Point[] points) /// Draws a . /// public void DrawPath(Pen pen, GraphicsPath path) - => m_canvas.DrawPath(path.m_path, pen.m_paint); + => PaintPath(path.m_path, pen.m_paint); /// /// Draws a pie shape defined by an ellipse specified by a structure and two radial lines. @@ -744,7 +752,7 @@ public void DrawPath(Pen pen, GraphicsPath path) public void DrawPie(Pen pen, RectangleF rect, float startAngle, float sweepAngle) { using var path = GetPiePath(rect, startAngle, sweepAngle); - m_canvas.DrawPath(path.m_path, pen.m_paint); + DrawPath(pen, path); } /// @@ -771,7 +779,7 @@ public void DrawPie(Pen pen, int x, int y, int width, int height, float startAng public void DrawPolygon(Pen pen, params PointF[] points) { using var path = GetPolygonPath(points, FillMode.Winding); - m_canvas.DrawPath(path.m_path, pen.m_paint); + DrawPath(pen, path); } /// @@ -784,7 +792,10 @@ public void DrawPolygon(Pen pen, params Point[] points) /// Draws a rectangle specified by a structure. /// public void DrawRectangle(Pen pen, RectangleF rect) - => m_canvas.DrawRect(rect.m_rect, pen.m_paint); + { + using var path = GetRectanglePath(rect); + DrawPath(pen, path); + } /// /// Draws a rectangle specified by a structure. @@ -859,7 +870,7 @@ public void DrawString(string text, Font font, Brush brush, RectangleF layout, S { using var path = new GraphicsPath(); path.AddString(text, font.FontFamily, (int)font.Style, font.Size, layout, format); - m_canvas.DrawPath(path.m_path, brush.m_paint); + FillPath(brush, path); } /// @@ -1142,7 +1153,7 @@ public void ExcludeClip(Rectangle rect) public void FillClosedCurve(Brush brush, PointF[] points, FillMode fillMode = FillMode.Alternate, float tension = 0.5f) { using var path = GetCurvePath(points, fillMode, tension, true); - m_canvas.DrawPath(path.m_path, brush.m_paint); + FillPath(brush, path); } /// @@ -1171,7 +1182,10 @@ public void FillEllipse(Brush brush, int x, int y, int width, int height) /// specified by a Rectangle structure. /// public void FillEllipse(Brush brush, RectangleF rect) - => m_canvas.DrawOval(rect.m_rect, brush.m_paint); + { + using var path = GetEllipsePath(rect); + FillPath(brush, path); + } /// /// Fills the interior of an ellipse defined by a bounding @@ -1184,7 +1198,7 @@ public void FillEllipse(Brush brush, Rectangle rect) /// Fills the interior of a . /// public void FillPath(Brush brush, GraphicsPath path) - => m_canvas.DrawPath(path.m_path, brush.m_paint); + => PaintPath(path.m_path, brush.m_paint); /// /// Fills the interior of a pie section defined by an ellipse specified by @@ -1193,7 +1207,7 @@ public void FillPath(Brush brush, GraphicsPath path) public void FillPie(Brush brush, RectangleF oval, float startAngle, float sweepAngle) { using var path = GetPiePath(oval, startAngle, sweepAngle); - m_canvas.DrawPath(path.m_path, brush.m_paint); + FillPath(brush, path); } /// @@ -1224,7 +1238,7 @@ public void FillPie(Brush brush, int x, int y, int width, int height, float star public void FillPolygon(Brush brush, PointF[] points, FillMode fillMode = FillMode.Alternate) { using var path = GetPolygonPath(points, fillMode); - m_canvas.DrawPath(path.m_path, brush.m_paint); + FillPath(brush, path); } /// @@ -1238,7 +1252,10 @@ public void FillPolygon(Brush brush, Point[] points, FillMode fillMode = FillMod /// Fills the interior of a rectangle specified by a structure. /// public void FillRectangle(Brush brush, RectangleF rect) - => m_canvas.DrawRect(rect.m_rect, brush.m_paint); + { + using var path = GetRectanglePath(rect); + FillPath(brush, path); + } /// /// Fills the interior of a rectangle specified by a structure. @@ -1274,7 +1291,11 @@ public void FillRectangles(Brush brush, params Rectangle[] rects) /// Fills the interior of a . /// public void FillRegion(Brush brush, Region region) - => m_canvas.DrawRegion(region.m_region, brush.m_paint); + { + using var boundaries = region.m_region.GetBoundaryPath(); + using var path = new GraphicsPath(boundaries); + FillPath(brush, path); + } /// /// Forces execution of all pending graphics operations with the method waiting @@ -1786,6 +1807,12 @@ internal static float GetFactor(float dpi, GraphicsUnit sourceUnit, GraphicsUnit private Region ClipRegion { get; set; } + private void PaintPath(SKPath path, SKPaint paint) + { + m_path.AddPath(path); // used by IsVisible method + m_canvas.DrawPath(path, paint); + } + private static GraphicsPath GetCurvePath(PointF[] points, FillMode fillMode, float tension, bool closed) { if (points.Length < 3) @@ -1800,6 +1827,13 @@ private static GraphicsPath GetCurvePath(PointF[] points, FillMode fillMode, flo return path; } + private static GraphicsPath GetEllipsePath(RectangleF rect) + { + var path = new GraphicsPath(); + path.AddEllipse(rect); + return path; + } + private static GraphicsPath GetPiePath(RectangleF rect, float startAngle, float sweepAngle) { var path = new GraphicsPath(); @@ -1814,6 +1848,13 @@ private static GraphicsPath GetPolygonPath(PointF[] points, FillMode fillMode) return path; } + private static GraphicsPath GetRectanglePath(RectangleF rect) + { + var path = new GraphicsPath(); + path.AddRectangle(rect); + return path; + } + private void TransformPoints(CoordinateSpace destination, CoordinateSpace source, T[] points, Func getPoint, Func newPoint) { if (source == destination) From 80c284e13a7abf7906d234e26ca52349dfe5c7b1 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 16:09:17 -0300 Subject: [PATCH 178/217] fix GraphicsPath.IsVisible method that must check if point/rect is contained in the shape defined in the canvas --- src/Common/Graphics.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 77f98a6..436a02d 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1373,7 +1373,7 @@ public void IntersectClip(Rectangle rect) /// within the visible clip region of this . /// public bool IsVisible(PointF point) - => m_canvas.LocalClipBounds.Contains(point.m_point); + => IsVisible(new RectangleF(point, new SizeF(1, 1))); /// /// Indicates whether the specified structure is contained @@ -1387,7 +1387,11 @@ public bool IsVisible(Point point) /// within the visible clip region of this . /// public bool IsVisible(RectangleF rect) - => m_canvas.LocalClipBounds.Contains(rect.m_rect); + { + using var path = new GraphicsPath(m_path); + using var region = new Region(path); + return region.IsVisible(rect); + } /// /// Indicates whether the specified structure is contained From 31e5870f54770d725cb40d5af32a03915543471a Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 16:11:29 -0300 Subject: [PATCH 179/217] fix Graphics' BeginContainer that must reset the class state until to be restored while Save applies an accumulative state --- src/Common/Graphics.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 436a02d..4fc4dfd 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -263,7 +263,7 @@ public GraphicsContainer BeginContainer() /// public GraphicsContainer BeginContainer(RectangleF dstRect, RectangleF srcRect, GraphicsUnit unit) { - int state = m_canvas.SaveLayer(); + int state = m_canvas.Save(); float factorX = GetFactor(DpiX, unit, GraphicsUnit.Pixel); float factorY = GetFactor(DpiY, unit, GraphicsUnit.Pixel); @@ -277,9 +277,11 @@ public GraphicsContainer BeginContainer(RectangleF dstRect, RectangleF srcRect, float translateX = dst.Left - src.Left * scaleX; float translateY = dst.Top - src.Top * scaleY; + m_canvas.ResetMatrix(); m_canvas.Translate(translateX, translateY); m_canvas.Scale(scaleX, scaleY); + // TODO: reset all properties of this instance and restore them when calling to EndContainer return new GraphicsContainer(state); } @@ -1656,7 +1658,7 @@ public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend /// identifies the saved state with a . /// public GraphicsState Save() - => new(m_canvas.Save()); + => new(m_canvas.SaveLayer()); /// /// Applies the specified scaling operation to the transformation matrix of From 67b989de857bf9527b587e327cb64b83a2f419df Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Tue, 10 Sep 2024 16:51:40 -0300 Subject: [PATCH 180/217] add support for Graphics.CompositingMode --- src/Common/Graphics.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 4fc4dfd..6a88464 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1815,6 +1815,15 @@ internal static float GetFactor(float dpi, GraphicsUnit sourceUnit, GraphicsUnit private void PaintPath(SKPath path, SKPaint paint) { + var render = paint.Clone(); + render.BlendMode = CompositingMode switch + { + CompositeMode.SourceOver => SKBlendMode.SrcOver, + CompositeMode.SourceCopy => SKBlendMode.Src, + _ => throw new NotImplementedException() + }; + + m_canvas.DrawPath(path, render); m_path.AddPath(path); // used by IsVisible method m_canvas.DrawPath(path, paint); } From ba3f202e18cd0bcb4d2e7864ad50500fda60824a Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 11 Sep 2024 09:52:45 -0300 Subject: [PATCH 181/217] fix HatchBrush shader rendering --- src/Common/Drawing2D/HatchBrush.cs | 4 ++-- test/Common/Drawing2D/HatchBrushUnitTest.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Common/Drawing2D/HatchBrush.cs b/src/Common/Drawing2D/HatchBrush.cs index 3260b93..652a264 100644 --- a/src/Common/Drawing2D/HatchBrush.cs +++ b/src/Common/Drawing2D/HatchBrush.cs @@ -132,12 +132,12 @@ private void UpdateShader(Action action) using var canvas = new SKCanvas(bitmap); using var paint = new SKPaint { - Color = m_back.m_color, + Color = m_fore.m_color, Style = SKPaintStyle.Fill, IsAntialias = false }; - canvas.Clear(m_fore.m_color); + canvas.Clear(m_back.m_color); for (int x = 0; x < data.Width; x++) { diff --git a/test/Common/Drawing2D/HatchBrushUnitTest.cs b/test/Common/Drawing2D/HatchBrushUnitTest.cs index 7d3c74f..502220f 100644 --- a/test/Common/Drawing2D/HatchBrushUnitTest.cs +++ b/test/Common/Drawing2D/HatchBrushUnitTest.cs @@ -66,8 +66,8 @@ public void Setup() [TestCase(HatchStyle.SolidDiamond)] public void Constructor_StyleForeBack(HatchStyle style) { - var back = Color.Red; - var fore = Color.Blue; + var fore = Color.Red; + var back = Color.Blue; using var brush = new HatchBrush(style, fore, back); Assert.Multiple(() => From 4f9f873d3904e5009af7e1c8213c1f9d75b6d4f6 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 11 Sep 2024 10:04:01 -0300 Subject: [PATCH 182/217] add support for Graphics.RenderingOrigin property --- src/Common/Drawing2D/HatchBrush.cs | 2 +- src/Common/Graphics.cs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Common/Drawing2D/HatchBrush.cs b/src/Common/Drawing2D/HatchBrush.cs index 652a264..2b644db 100644 --- a/src/Common/Drawing2D/HatchBrush.cs +++ b/src/Common/Drawing2D/HatchBrush.cs @@ -15,7 +15,7 @@ public sealed class HatchBrush : Brush /// specified enumeration, foreground color, and background color. /// public HatchBrush(HatchStyle hatchStyle, Color foreColor, Color backColor) - : base(new SKPaint { }) + : base(new SKPaint { IsDither = true }) { m_fore = foreColor; m_back = backColor; diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 6a88464..cd2edb2 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1815,7 +1815,7 @@ internal static float GetFactor(float dpi, GraphicsUnit sourceUnit, GraphicsUnit private void PaintPath(SKPath path, SKPaint paint) { - var render = paint.Clone(); + using var render = paint.Clone(); render.BlendMode = CompositingMode switch { CompositeMode.SourceOver => SKBlendMode.SrcOver, @@ -1823,9 +1823,14 @@ private void PaintPath(SKPath path, SKPaint paint) _ => throw new NotImplementedException() }; + if (render.IsDither) + { + var translation = SKMatrix.CreateTranslation(RenderingOrigin.X, RenderingOrigin.Y); + render.Shader = render.Shader.WithLocalMatrix(translation); + } + m_canvas.DrawPath(path, render); m_path.AddPath(path); // used by IsVisible method - m_canvas.DrawPath(path, paint); } private static GraphicsPath GetCurvePath(PointF[] points, FillMode fillMode, float tension, bool closed) From 8b05c2d25fa894ca5d37c9ec740c5ce424591231 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 11 Sep 2024 10:15:26 -0300 Subject: [PATCH 183/217] fix CompositingMode enum name --- .../Drawing2D/{CompositeMode.cs => CompositingMode.cs} | 2 +- src/Common/Graphics.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/Common/Drawing2D/{CompositeMode.cs => CompositingMode.cs} (94%) diff --git a/src/Common/Drawing2D/CompositeMode.cs b/src/Common/Drawing2D/CompositingMode.cs similarity index 94% rename from src/Common/Drawing2D/CompositeMode.cs rename to src/Common/Drawing2D/CompositingMode.cs index b71746e..8193751 100644 --- a/src/Common/Drawing2D/CompositeMode.cs +++ b/src/Common/Drawing2D/CompositingMode.cs @@ -3,7 +3,7 @@ namespace GeneXus.Drawing.Drawing2D; /// /// Specifies how the source colors are combined with the background colors. /// -public enum CompositeMode +public enum CompositingMode { /// /// Specifies that when a color is rendered, it overwrites the background color. diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index cd2edb2..8b751a5 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -98,7 +98,7 @@ public Region Clip /// /// Gets a value that specifies how composited images are drawn to this . /// - public CompositeMode CompositingMode { get; set; } = CompositeMode.SourceOver; + public CompositingMode CompositingMode { get; set; } = CompositingMode.SourceOver; /// /// Gets or sets the rendering quality of composited images drawn to this . @@ -1818,8 +1818,8 @@ private void PaintPath(SKPath path, SKPaint paint) using var render = paint.Clone(); render.BlendMode = CompositingMode switch { - CompositeMode.SourceOver => SKBlendMode.SrcOver, - CompositeMode.SourceCopy => SKBlendMode.Src, + CompositingMode.SourceOver => SKBlendMode.SrcOver, + CompositingMode.SourceCopy => SKBlendMode.Src, _ => throw new NotImplementedException() }; From 3484543bff74c31bfca76c1020749ea776cbe513 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Wed, 11 Sep 2024 11:31:37 -0300 Subject: [PATCH 184/217] add support for Graphics.InterpolationMode property --- src/Common/Graphics.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 8b751a5..c0009ec 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1822,6 +1822,22 @@ private void PaintPath(SKPath path, SKPaint paint) CompositingMode.SourceCopy => SKBlendMode.Src, _ => throw new NotImplementedException() }; + render.FilterQuality = InterpolationMode switch + { + InterpolationMode.NearestNeighbor + => SKFilterQuality.None, + InterpolationMode.Low + => SKFilterQuality.Low, + InterpolationMode.Default or + InterpolationMode.Bilinear + => SKFilterQuality.Medium, + InterpolationMode.High or + InterpolationMode.Bicubic or + InterpolationMode.HighQualityBilinear or + InterpolationMode.HighQualityBicubic + => SKFilterQuality.High, + _ => throw new NotImplementedException() + }; if (render.IsDither) { From f4c453ef73b31da7bb1ee3ee5017da52a7783781 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 21:37:31 -0300 Subject: [PATCH 185/217] fix StringFormat.GenericTypographic property default flags --- src/Common/StringFormat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/StringFormat.cs b/src/Common/StringFormat.cs index 7d65939..4ad5014 100644 --- a/src/Common/StringFormat.cs +++ b/src/Common/StringFormat.cs @@ -114,7 +114,7 @@ private void Dispose(bool disposing) { } /// public static StringFormat GenericTypographic => new() { - FormatFlags = StringFormatFlags.FitBlackBox | StringFormatFlags.LineLimit, + FormatFlags = StringFormatFlags.FitBlackBox | StringFormatFlags.LineLimit | StringFormatFlags.NoClip, Trimming = StringTrimming.None }; From 4ef1f642bea13b0ad2f83e753088df55367afef3 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 21:38:25 -0300 Subject: [PATCH 186/217] fix StringFormat border case when applying wrapping and avoid subpixel render --- src/Common/StringFormat.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/StringFormat.cs b/src/Common/StringFormat.cs index 4ad5014..7a4fdf7 100644 --- a/src/Common/StringFormat.cs +++ b/src/Common/StringFormat.cs @@ -377,7 +377,7 @@ internal string ApplyWrapping(string text, SKRect boundBox, Func foreach (var word in line.Split(' ')) { var curWidth = measureText(word); - if ((accWidth += curWidth) > boundBox.Width) + if (Math.Ceiling(accWidth += curWidth) >= boundBox.Width) { sb.Append('\n').Append(' '); accWidth = curWidth; @@ -414,7 +414,7 @@ private string ApplyTrimming(IEnumerable tokens, string endToken, SKRect } } - if ((accWidth += curWidth) + endLen > boundBox.Width) + if (Math.Ceiling((accWidth += curWidth) + endLen) >= boundBox.Width) { if (FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft)) sb.Insert(0, endToken); From 49000400add4d2eed731e65587f30337ca39af1f Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 22:12:59 -0300 Subject: [PATCH 187/217] fix Graphics.MeasureCharacterRanges method in order to consider MeasurableRanges in StingFormat --- src/Common/Graphics.cs | 21 ++++++--------------- src/Common/StringFormat.cs | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index c0009ec..f658b81 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1443,27 +1443,18 @@ public Region[] MeasureCharacterRanges(ReadOnlySpan text, Font font, Recta /// public Region[] MeasureCharacterRanges(string text, Font font, RectangleF layout, StringFormat format) { - // TODO: implement StringFormat if (text == null) throw new ArgumentNullException(nameof(text)); if (font == null) throw new ArgumentNullException(nameof(font)); - var localBounds = new RectangleF(m_canvas.LocalClipBounds); - var totalBounds = RectangleF.Intersect(localBounds, layout); - var regions = new List(); - var lines = text.Split(StringFormat.BREAKLINES, StringSplitOptions.None); - - foreach (var line in lines) + foreach (var substring in format.ApplyRanges(text)) { - foreach (var character in line) - { - var path = new GraphicsPath(); - path.AddString(character.ToString(), font.FontFamily, (int)font.Style, font.Size, totalBounds, format); + using var path = new GraphicsPath(); + path.AddString(substring, font.FontFamily, (int)font.Style, font.Size * DpiY / 72, layout, format); - var charBounds = path.GetBounds(); - var charRegion = new Region(charBounds); - regions.Add(charRegion); - } + var bounds = path.GetBounds(); + var region = new Region(bounds); + regions.Add(region); } return regions.ToArray(); diff --git a/src/Common/StringFormat.cs b/src/Common/StringFormat.cs index 7a4fdf7..25a1e44 100644 --- a/src/Common/StringFormat.cs +++ b/src/Common/StringFormat.cs @@ -269,6 +269,21 @@ public void SetTabStops(float firstTabOffset, float[] tabStops) #region Helpers + internal string[] ApplyRanges(string text) + { + var substrings = new List(); + foreach (var range in Ranges) + { + int idx = Math.Max(0, range.First); + int end = Math.Min(idx + range.Length, idx + text.Length); + if (idx == end) + continue; + string substring = text.Substring(idx, end - idx); + substrings.Add(substring); + } + return substrings.ToArray(); + } + internal string ApplyDirection(string text) => FormatFlags.HasFlag(StringFormatFlags.DirectionVertical) ? string.Join("\n", text.Split(BREAKLINES, StringSplitOptions.None).Reverse()) From 1a145eadbc0b3339cd1716f983edf18cc526abf9 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 22:18:53 -0300 Subject: [PATCH 188/217] fix Graphics.DrawString overloads calls --- src/Common/Graphics.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index f658b81..20db3d9 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -848,14 +848,14 @@ public void DrawString(string text, Font font, Brush brush, float x, float y, St /// objects using the formatting attributes of the specified StringFormat. /// text, Font font, Brush brush, PointF point, StringFormat format = null) - => DrawString(new string(text.ToArray()), font, brush, new RectangleF(point.X, point.Y, 0, 0), format); + => DrawString(new string(text.ToArray()), font, brush, point, format); /// /// Draws the specified text string in the specified location with the specified and /// objects using the formatting attributes of the specified StringFormat. /// DrawString(text, font, brush, new RectangleF(point.X, point.Y, 0, 0), format); + => DrawString(text, font, brush, new RectangleF(point, float.PositiveInfinity, float.PositiveInfinity), format); /// /// Draws the specified text string in the specified rectangle with the specified and @@ -871,7 +871,7 @@ public void DrawString(ReadOnlySpan text, Font font, Brush brush, Rectangl public void DrawString(string text, Font font, Brush brush, RectangleF layout, StringFormat format = null) { using var path = new GraphicsPath(); - path.AddString(text, font.FontFamily, (int)font.Style, font.Size, layout, format); + path.AddString(text, font.FontFamily, (int)font.Style, font.Size * DpiY / 72, layout, format); FillPath(brush, path); } From 763d0ba005b568f39007e8e9d09c75f050c3eb63 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 22:19:28 -0300 Subject: [PATCH 189/217] fix Graphics.MeasureString overload calls --- src/Common/Graphics.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 20db3d9..7664141 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1474,7 +1474,7 @@ public SizeF MeasureString(string text, Font font, RectangleF layoutArea = defau /// a structure. /// public SizeF MeasureString(string text, Font font, SizeF layoutArea, StringFormat format = null) - => MeasureString(text, font, new RectangleF(default, layoutArea), format); + => MeasureString(text, font, new RectangleF(0, 0, layoutArea), format); /// /// Measures the specified string when drawn with the specified from an origin , formatted @@ -1482,7 +1482,7 @@ public SizeF MeasureString(string text, Font font, SizeF layoutArea, StringForma /// a structure. /// public SizeF MeasureString(string text, Font font, PointF origin, StringFormat format = null) - => MeasureString(text, font, new RectangleF(origin, default), format); + => MeasureString(text, font, new RectangleF(origin, 0, 0), format); /// /// Measures the specified string when drawn with the specified and certain width area, formatted From dc374eec488c3426eeeb8ddf9b66aa214d913f7c Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 22:19:53 -0300 Subject: [PATCH 190/217] add support for Graphics.SmoothingMode property --- src/Common/Graphics.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 7664141..efcd1f3 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1829,6 +1829,7 @@ InterpolationMode.HighQualityBilinear or => SKFilterQuality.High, _ => throw new NotImplementedException() }; + render.IsAntialias = SmoothingMode == SmoothingMode.AntiAlias; if (render.IsDither) { From b46fd46a7f21e9c74fdfe8994e868b2e85e8d9ed Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 22:21:42 -0300 Subject: [PATCH 191/217] fix GraphicsPath.AddString overload calls --- src/Common/Drawing2D/GraphicsPath.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 3328ab3..7d864be 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -498,13 +498,13 @@ public void AddString(string text, FontFamily family, int style, float emSize, R /// Adds a text string to this path starting from a structure. /// public void AddString(string text, FontFamily family, int style, float emSize, PointF origin, StringFormat format) - => AddString(text, family, style, emSize, new RectangleF(origin, float.MaxValue, float.MaxValue), format); + => AddString(text, family, style, emSize, new RectangleF(origin, float.PositiveInfinity, float.PositiveInfinity), format); /// /// Adds a text string to this path starting from a structure. /// public void AddString(string text, FontFamily family, int style, float emSize, Point origin, StringFormat format) - => AddString(text, family, style, emSize, new Rectangle(origin, int.MaxValue, int.MaxValue), format); + => AddString(text, family, style, emSize, new PointF(origin.m_point), format); /// /// Clears all markers from this path. From 64e145585b39c5c15ccc765a75d535b997661813 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 22:47:19 -0300 Subject: [PATCH 192/217] fix and improve readability of AddString internal method that applies StringFormat to get the text path --- src/Common/Drawing2D/GraphicsPath.cs | 84 +++++++++++++++------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 7d864be..4ab673d 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -913,14 +913,16 @@ private void AddRectangle(SKRect rect) private void AddString(string text, FontFamily family, int style, float emSize, SKRect layout, StringFormat format) { - format ??= new StringFormat(); + if (string.IsNullOrEmpty(text)) return; + format ??= new StringFormat(StringFormatFlags.NoWrap | StringFormatFlags.NoClip); bool isRightToLeft = format.FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft); - using var paint = new SKPaint + using var typeface = family.GetTypeface((FontStyle)style); + using var font = new SKFont(typeface, (int)emSize); + + using var paint = new SKPaint(font) { - Typeface = family.GetTypeface((FontStyle)style), - TextSize = emSize, TextAlign = format.Alignment switch { StringAlignment.Near => isRightToLeft ? SKTextAlign.Right : SKTextAlign.Left, @@ -931,14 +933,6 @@ private void AddString(string text, FontFamily family, int style, float emSize, Style = SKPaintStyle.Stroke }; - float baselineHeight = -paint.FontMetrics.Ascent; - float underlineOffset = paint.FontMetrics.UnderlinePosition ?? 1.8f; - float underlineHeight = paint.FontMetrics.UnderlineThickness ?? paint.GetTextPath("_", 0, 0).Bounds.Height; - float paragraphOffset = paint.FontSpacing - baselineHeight - underlineHeight - underlineOffset; - - // define offset based on System.Drawing (based on trial/error comparison) - float offsetX = isRightToLeft ? 0 : 5, offsetY = baselineHeight - 6; - // apply format to the string text = format.ApplyDirection(text); text = format.ApplyTabStops(text); @@ -948,49 +942,60 @@ private void AddString(string text, FontFamily family, int style, float emSize, text = format.ApplyTrimming(text, layout, paint.FontSpacing, paint.MeasureText); text = format.ApplyHotkey(text, out var underlines); + // calculate line and underline vertical sizes (https://fiddle.skia.org/i/b5b76e0a15da0c3530071186a9006498_raster.png) + float lineHeight = paint.FontMetrics.Bottom - paint.FontMetrics.Top; + float underlineOffset = paint.FontMetrics.UnderlinePosition ?? 1.8f; + float underlineHeight = paint.FontMetrics.UnderlineThickness ?? paint.GetTextPath("_", 0, 0).Bounds.Height; + + underlineOffset -= underlineHeight / 2; + // create returning path var path = new SKPath(); // get path for current text, including breaklines and underlines - float lineHeightOffset = 0f; + float lineHeightOffset = 1f, lineWidthOffset = 2f; // NOTE: these values were gotten from trial & error int lineIndexOffset = 0; - int breaklineOffset = format.FormatFlags.HasFlag(StringFormatFlags.NoWrap) ? 1 : 0; - foreach (string line in text.Split(StringFormat.BREAKLINES, StringSplitOptions.None)) + foreach (string textLine in text.Split(StringFormat.BREAKLINES, StringSplitOptions.None)) { // check if the line fits within the layout height - if (format.FormatFlags.HasFlag(StringFormatFlags.LineLimit) && lineHeightOffset + baselineHeight > layout.Height) + if (format.FormatFlags.HasFlag(StringFormatFlags.LineLimit) && lineHeightOffset + lineHeight > layout.Height) break; // get text path for current line - var linePath = paint.GetTextPath(line, 0, lineHeightOffset); + var textPath = paint.GetTextPath(textLine, 0, 0); - // Adjust horizontal alignments for right-to-left text - float rtlOffset = isRightToLeft ? layout.Width - paint.MeasureText(line) - 5 : 0; - linePath.Offset(rtlOffset, 0); + // adjust horizontal alignments for right-to-left text + float rtlOffsetX = isRightToLeft ? layout.Width - paint.MeasureText(textLine) - lineWidthOffset : 0; + textPath.Offset(rtlOffsetX, 0); // get rect path for each underline defined by hotkey prefix - foreach (var index in underlines.Where(idx => idx >= lineIndexOffset && idx < lineIndexOffset + line.Length)) + float underlineTop = (int)(lineHeightOffset + underlineOffset); + foreach (var underlineIndex in underlines.Where(idx => idx >= lineIndexOffset && idx < lineIndexOffset + textLine.Length)) { - int relIndex = index - lineIndexOffset; - if (isRightToLeft && relIndex == 0 && line[relIndex] == '…') - relIndex += 1; // TODO: look for a better fix for this (in rtl) + int relativeIndex = underlineIndex - lineIndexOffset; - float origin = paint.MeasureText(line.Substring(0, relIndex)); - float length = paint.MeasureText(line.Substring(relIndex, 1)); + float origin = paint.MeasureText(textLine.Substring(0, relativeIndex)); + float length = paint.MeasureText(textLine.Substring(relativeIndex, 1)); var underline = new SKRect( - origin + rtlOffset, - lineHeightOffset + underlineOffset, - origin + length + rtlOffset, - lineHeightOffset + underlineOffset + underlineHeight); - linePath.AddRect(underline); + origin + rtlOffsetX, + underlineTop, + origin + length + rtlOffsetX, + underlineTop + underlineHeight); + textPath.AddRect(underline); } + // translate path + textPath.Offset( + -textPath.Bounds.Left + lineWidthOffset + rtlOffsetX, + -paint.FontMetrics.Ascent + lineHeightOffset); + // add line path - path.AddPath(linePath); + path.AddPath(textPath); - lineIndexOffset += line.Length + breaklineOffset; - lineHeightOffset += baselineHeight + paragraphOffset; + // update offsets + lineIndexOffset += textLine.Length + "\n".Length; + lineHeightOffset += lineHeight; } // align path vertically @@ -1005,21 +1010,19 @@ private void AddString(string text, FontFamily family, int style, float emSize, float fitOffsetX = isRightToLeft ? Math.Min(0, layout.Right - path.Bounds.Right) : Math.Max(0, layout.Left - path.Bounds.Left); - path.Offset(fitOffsetX, 0); + path.Offset(fitOffsetX - lineWidthOffset, 0); } - // apply offset reltive to layout - path.Offset(layout.Left + offsetX, layout.Top + offsetY); - // apply rotation and offset if required if (format.FormatFlags.HasFlag(StringFormatFlags.DirectionVertical)) { path.Transform(SKMatrix.CreateRotationDegrees(90)); - path.Offset(path.Bounds.Standardized.Height + paragraphOffset + underlineOffset + underlineHeight, 0); + float rotOffsetX = path.Bounds.Standardized.Height + underlineOffset + underlineHeight; + path.Offset(rotOffsetX, 0); } // apply clip if required - if (!format.FormatFlags.HasFlag(StringFormatFlags.NoClip)) + if (!format.FormatFlags.HasFlag(StringFormatFlags.NoClip) && layout.Width > 0 && layout.Height > 0) { var clip = new SKPath(); clip.AddRect(new SKRect( @@ -1030,6 +1033,7 @@ private void AddString(string text, FontFamily family, int style, float emSize, path = path.Op(clip, SKPathOp.Intersect); } + // add text path to this path m_path.AddPath(path); } From 99baf270d27b9897bee7aef229cf0b91165192e9 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 22:48:31 -0300 Subject: [PATCH 193/217] fix Graphics.MeasureStringInternal method that's not considering total sizes when measuring the string --- src/Common/Graphics.cs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index efcd1f3..e328b1a 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1554,22 +1554,20 @@ public SizeF MeasureStringInternal(ReadOnlySpan text, Font font, Rectangle public SizeF MeasureStringInternal(string text, Font font, RectangleF layoutArea, StringFormat format, out int charsFitted, out int linesFilled) { using var path = new GraphicsPath(); - path.AddString(text, font.FontFamily, (int)font.Style, font.Size, layoutArea, format); + path.AddString(text, font.FontFamily, (int)font.Style, font.Size * DpiY / 72, layoutArea, format); var bounds = path.GetBounds(); + var lines = text.Split(StringFormat.BREAKLINES, StringSplitOptions.None); - var charWidth = bounds.Width / text.Split(StringFormat.BREAKLINES, StringSplitOptions.None).Max(line => line.Length); - var availableWidth = Math.Min(layoutArea.Width, m_canvas.LocalClipBounds.Width); - charsFitted = (int)(availableWidth / charWidth); + float totalWidth = bounds.Width + 2 * bounds.Left; + float charWidth = totalWidth / lines.Max(line => line.Length); + charsFitted = (int)(totalWidth / charWidth); - var lineHeight = bounds.Height; - var availableHeight = Math.Min(layoutArea.Height, m_canvas.LocalClipBounds.Height); - linesFilled = (int)(availableHeight / lineHeight); + float totalHeight = bounds.Height + 2 * bounds.Top; + float lineHeight = totalHeight / lines.Length; + linesFilled = (int)(totalHeight / lineHeight); - var width = Math.Min(bounds.Width, availableWidth); - var height = Math.Min(bounds.Height, availableHeight); - - return new SizeF(width, height); + return new SizeF(totalWidth, totalHeight); } /// From 1cf5bd3045dd6ab01468f256cec6aacea8b90efa Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 23:11:57 -0300 Subject: [PATCH 194/217] fix Gracphis.IsVisible disposable reference by making a copy --- src/Common/Graphics.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index e328b1a..956b27f 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1390,7 +1390,8 @@ public bool IsVisible(Point point) /// public bool IsVisible(RectangleF rect) { - using var path = new GraphicsPath(m_path); + using var copy = new SKPath(m_path); + using var path = new GraphicsPath(copy); using var region = new Region(path); return region.IsVisible(rect); } From d1ccf87e4959d3794010fc0fd99129b0de83914f Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Thu, 12 Sep 2024 23:40:40 -0300 Subject: [PATCH 195/217] consider font Unit in Graphics' DrawString, MeasureCharacterRanges and MeasureStringInternal methods --- src/Common/Graphics.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index 956b27f..a0b360d 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -870,8 +870,10 @@ public void DrawString(ReadOnlySpan text, Font font, Brush brush, Rectangl /// public void DrawString(string text, Font font, Brush brush, RectangleF layout, StringFormat format = null) { + float emSize = DpiY * GetFactor(font.Size, font.Unit, GraphicsUnit.Pixel); + using var path = new GraphicsPath(); - path.AddString(text, font.FontFamily, (int)font.Style, font.Size * DpiY / 72, layout, format); + path.AddString(text, font.FontFamily, (int)font.Style, emSize, layout, format); FillPath(brush, path); } @@ -1447,11 +1449,13 @@ public Region[] MeasureCharacterRanges(string text, Font font, RectangleF layout if (text == null) throw new ArgumentNullException(nameof(text)); if (font == null) throw new ArgumentNullException(nameof(font)); + float emSize = DpiY * GetFactor(font.Size, font.Unit, GraphicsUnit.Pixel); + var regions = new List(); foreach (var substring in format.ApplyRanges(text)) { using var path = new GraphicsPath(); - path.AddString(substring, font.FontFamily, (int)font.Style, font.Size * DpiY / 72, layout, format); + path.AddString(substring, font.FontFamily, (int)font.Style, emSize, layout, format); var bounds = path.GetBounds(); var region = new Region(bounds); @@ -1554,8 +1558,10 @@ public SizeF MeasureStringInternal(ReadOnlySpan text, Font font, Rectangle /// public SizeF MeasureStringInternal(string text, Font font, RectangleF layoutArea, StringFormat format, out int charsFitted, out int linesFilled) { + float emSize = DpiY * GetFactor(font.Size, font.Unit, GraphicsUnit.Pixel); + using var path = new GraphicsPath(); - path.AddString(text, font.FontFamily, (int)font.Style, font.Size * DpiY / 72, layoutArea, format); + path.AddString(text, font.FontFamily, (int)font.Style, emSize, layoutArea, format); var bounds = path.GetBounds(); var lines = text.Split(StringFormat.BREAKLINES, StringSplitOptions.None); From 96c75455534fde08b37c0889feb3cbae89428fa8 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Fri, 13 Sep 2024 16:29:27 -0300 Subject: [PATCH 196/217] fix lineHeight calculation in GraphicsPath.AddString method --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 4ab673d..e34f271 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -943,7 +943,7 @@ private void AddString(string text, FontFamily family, int style, float emSize, text = format.ApplyHotkey(text, out var underlines); // calculate line and underline vertical sizes (https://fiddle.skia.org/i/b5b76e0a15da0c3530071186a9006498_raster.png) - float lineHeight = paint.FontMetrics.Bottom - paint.FontMetrics.Top; + float lineHeight = paint.FontMetrics.Descent - paint.FontMetrics.Ascent + paint.FontMetrics.Leading; float underlineOffset = paint.FontMetrics.UnderlinePosition ?? 1.8f; float underlineHeight = paint.FontMetrics.UnderlineThickness ?? paint.GetTextPath("_", 0, 0).Bounds.Height; From 970c79438233fcee99da0b19b81ded17cd7264e1 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 10:44:02 -0300 Subject: [PATCH 197/217] add ColorTranslator class --- README.md | 1 + src/Common/ColorTranslator.cs | 77 ++++++++++++++++++++++++++ test/Common/ColorTranslatorUnitTest.cs | 63 +++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/Common/ColorTranslator.cs create mode 100644 test/Common/ColorTranslatorUnitTest.cs diff --git a/README.md b/README.md index 3bac890..a4fd3e6 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Basic graphics funcionalities based on `System.Drawing`. | `Bitmap` | Class | Represents an image defined by pixels. | `Brush` | Class | Abstract class for brushes used to fill graphics shapes. | `Color` | Class | Defines colors used for drawing. +| `ColorTranslator` | Class | Translates colors to and from HTML, OLE and Win32 representations. | `Font` | Class | Defines a format for text, including font family, size, and style. | `Graphics` | Class | Provides methods for drawing on a drawing surface. | `Icon` | Class | Represents an icon image. diff --git a/src/Common/ColorTranslator.cs b/src/Common/ColorTranslator.cs new file mode 100644 index 0000000..0777623 --- /dev/null +++ b/src/Common/ColorTranslator.cs @@ -0,0 +1,77 @@ +using System; +using System.Globalization; + +namespace GeneXus.Drawing; + +/// +/// Translates colors to and from GDI+ objects. +/// +public static class ColorTranslator +{ + // COLORREF is 0x00BBGGRR + private const int R_SHIFT = 0, G_SHIFT = 8, B_SHIFT = 16; + private const int OLE_SYS_COLOR_FLAG = unchecked((int)0x80000000); + + #region Methods + + /// + /// Translates the specified to an Ole color. + /// + public static int ToOle(Color c) + => c.IsKnownColor && c.IsSystemColor ? throw new NotImplementedException() : ToWin32(c); + + /// + /// Translates the specified to an Html string color representation. + /// + public static string ToHtml(Color c) + => c.IsNamedColor ? c.Name : c.Hex; + + /// + /// Translates the specified to a Win32 color. + /// + public static int ToWin32(Color c) + => c.R << R_SHIFT | c.G << G_SHIFT | c.B << B_SHIFT; + + /// + /// Translates an Html color representation to a . + /// + public static Color FromHtml(string htmlColor) + { + if (htmlColor.StartsWith("#")) + return Color.FromHex(htmlColor); + + if (htmlColor.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || htmlColor.StartsWith("&h", StringComparison.OrdinalIgnoreCase)) + return Color.FromArgb(Convert.ToInt32(htmlColor.Substring(2), 16)); + + var provider = (NumberFormatInfo)CultureInfo.CurrentCulture.GetFormat(typeof(NumberFormatInfo)); + if (int.TryParse(htmlColor, NumberStyles.Integer, provider, out int value)) + return Color.FromArgb(value); + + return Color.FromName(htmlColor); + } + + /// + /// Translates an Ole color value to a . + /// + public static Color FromOle(int oleColor) + => (oleColor & OLE_SYS_COLOR_FLAG) == 0 ? Color.FromArgb((int)RefToArgb((uint)oleColor)) : throw new NotImplementedException(); + + /// + /// Translates an Win32 color value to a . + /// + public static Color FromWin32(int win32Color) + => FromOle(win32Color); + + #endregion + + + #region Helpers + + private static uint RefToArgb(uint value) + => ((value >> R_SHIFT) & 0xFF) << 16 + | ((value >> G_SHIFT) & 0xFF) << 8 + | ((value >> B_SHIFT) & 0xFF) << 0 + | 0xFFu << 24; + + #endregion +} \ No newline at end of file diff --git a/test/Common/ColorTranslatorUnitTest.cs b/test/Common/ColorTranslatorUnitTest.cs new file mode 100644 index 0000000..7eab790 --- /dev/null +++ b/test/Common/ColorTranslatorUnitTest.cs @@ -0,0 +1,63 @@ +namespace GeneXus.Drawing.Test; + +internal class ColorTranslatorUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Method_ToHtml() + { + var color = Color.Red; + var htmlColor = ColorTranslator.ToHtml(color); + + Assert.That(htmlColor, Is.EqualTo("#F00")); + } + + [Test] + public void Method_FromHtml() + { + var htmlColor = "#FF0000"; + var color = ColorTranslator.FromHtml(htmlColor); + + Assert.That(color, Is.EqualTo(Color.Red)); + } + + [Test] + public void Method_ToOle() + { + var color = Color.Blue; + var oleColor = ColorTranslator.ToOle(color); + + Assert.That(oleColor, Is.EqualTo(0xFF0000)); + } + + [Test] + public void Method_FromOle() + { + var oleColor = 0xFF0000; + var color = ColorTranslator.FromOle(oleColor); + + Assert.That(color, Is.EqualTo(Color.Blue)); + } + + [Test] + public void Method_ToWin32() + { + var color = Color.Lime; + var win32Color = ColorTranslator.ToWin32(color); + + Assert.That(win32Color, Is.EqualTo(0x00FF00)); + } + + [Test] + public void Method_FromWin32() + { + var win32Color = 0x00FF00; + var color = ColorTranslator.FromWin32(win32Color); + + Assert.That(color, Is.EqualTo(Color.Lime)); + } +} \ No newline at end of file From 4fe30e9eda197e62fa928134e8e99f0600f22772 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 12:11:42 -0300 Subject: [PATCH 198/217] fix Color class for known colors by setting the name and index --- src/Common/Color.cs | 287 ++++++++++++++++++++++---------------------- 1 file changed, 145 insertions(+), 142 deletions(-) diff --git a/src/Common/Color.cs b/src/Common/Color.cs index c4565dc..87082f0 100644 --- a/src/Common/Color.cs +++ b/src/Common/Color.cs @@ -33,6 +33,9 @@ internal Color(SKColor color, string name = null, int index = 0) m_index = index; } + private Color(SKColor color, KnownColor knownColor) + : this(color, knownColor.ToString(), (int)knownColor) { } + /// /// Initializes a new instance of the structure with the specified /// alpha, red, green and blue values. @@ -379,148 +382,148 @@ private static SKColor CreateFromHex(string hex, bool argb) #region NamedColors - public static Color Transparent => new(SKColors.Transparent); - public static Color AliceBlue => new(SKColors.AliceBlue); - public static Color AntiqueWhite => new(SKColors.AntiqueWhite); - public static Color Aqua => new(SKColors.Aqua); - public static Color Aquamarine => new(SKColors.Aquamarine); - public static Color Azure => new(SKColors.Azure); - public static Color Beige => new(SKColors.Beige); - public static Color Bisque => new(SKColors.Bisque); - public static Color Black => new(SKColors.Black); - public static Color BlanchedAlmond => new(SKColors.BlanchedAlmond); - public static Color Blue => new(SKColors.Blue); - public static Color BlueViolet => new(SKColors.BlueViolet); - public static Color Brown => new(SKColors.Brown); - public static Color BurlyWood => new(SKColors.BurlyWood); - public static Color CadetBlue => new(SKColors.CadetBlue); - public static Color Chartreuse => new(SKColors.Chartreuse); - public static Color Chocolate => new(SKColors.Chocolate); - public static Color Coral => new(SKColors.Coral); - public static Color CornflowerBlue => new(SKColors.CornflowerBlue); - public static Color Cornsilk => new(SKColors.Cornsilk); - public static Color Crimson => new(SKColors.Crimson); - public static Color Cyan => new(SKColors.Cyan); - public static Color DarkBlue => new(SKColors.DarkBlue); - public static Color DarkCyan => new(SKColors.DarkCyan); - public static Color DarkGoldenrod => new(SKColors.DarkGoldenrod); - public static Color DarkGray => new(SKColors.DarkGray); - public static Color DarkGreen => new(SKColors.DarkGreen); - public static Color DarkKhaki => new(SKColors.DarkKhaki); - public static Color DarkMagenta => new(SKColors.DarkMagenta); - public static Color DarkOliveGreen => new(SKColors.DarkOliveGreen); - public static Color DarkOrange => new(SKColors.DarkOrange); - public static Color DarkOrchid => new(SKColors.DarkOrchid); - public static Color DarkRed => new(SKColors.DarkRed); - public static Color DarkSalmon => new(SKColors.DarkSalmon); - public static Color DarkSeaGreen => new(SKColors.DarkSeaGreen); - public static Color DarkSlateBlue => new(SKColors.DarkSlateBlue); - public static Color DarkSlateGray => new(SKColors.DarkSlateGray); - public static Color DarkTurquoise => new(SKColors.DarkTurquoise); - public static Color DarkViolet => new(SKColors.DarkViolet); - public static Color DeepPink => new(SKColors.DeepPink); - public static Color DeepSkyBlue => new(SKColors.DeepSkyBlue); - public static Color DimGray => new(SKColors.DimGray); - public static Color DodgerBlue => new(SKColors.DodgerBlue); - public static Color Firebrick => new(SKColors.Firebrick); - public static Color FloralWhite => new(SKColors.FloralWhite); - public static Color ForestGreen => new(SKColors.ForestGreen); - public static Color Fuchsia => new(SKColors.Fuchsia); - public static Color Gainsboro => new(SKColors.Gainsboro); - public static Color GhostWhite => new(SKColors.GhostWhite); - public static Color Gold => new(SKColors.Gold); - public static Color Goldenrod => new(SKColors.Goldenrod); - public static Color Gray => new(SKColors.Gray); - public static Color Green => new(SKColors.Green); - public static Color GreenYellow => new(SKColors.GreenYellow); - public static Color Honeydew => new(SKColors.Honeydew); - public static Color HotPink => new(SKColors.HotPink); - public static Color IndianRed => new(SKColors.IndianRed); - public static Color Indigo => new(SKColors.Indigo); - public static Color Ivory => new(SKColors.Ivory); - public static Color Khaki => new(SKColors.Khaki); - public static Color Lavender => new(SKColors.Lavender); - public static Color LavenderBlush => new(SKColors.LavenderBlush); - public static Color LawnGreen => new(SKColors.LawnGreen); - public static Color LemonChiffon => new(SKColors.LemonChiffon); - public static Color LightBlue => new(SKColors.LightBlue); - public static Color LightCoral => new(SKColors.LightCoral); - public static Color LightCyan => new(SKColors.LightCyan); - public static Color LightGoldenrodYellow => new(SKColors.LightGoldenrodYellow); - public static Color LightGray => new(SKColors.LightGray); - public static Color LightGreen => new(SKColors.LightGreen); - public static Color LightPink => new(SKColors.LightPink); - public static Color LightSalmon => new(SKColors.LightSalmon); - public static Color LightSeaGreen => new(SKColors.LightSeaGreen); - public static Color LightSkyBlue => new(SKColors.LightSkyBlue); - public static Color LightSlateGray => new(SKColors.LightSlateGray); - public static Color LightSteelBlue => new(SKColors.LightSteelBlue); - public static Color LightYellow => new(SKColors.LightYellow); - public static Color Lime => new(SKColors.Lime); - public static Color LimeGreen => new(SKColors.LimeGreen); - public static Color Linen => new(SKColors.Linen); - public static Color Magenta => new(SKColors.Magenta); - public static Color Maroon => new(SKColors.Maroon); - public static Color MediumAquamarine => new(SKColors.MediumAquamarine); - public static Color MediumBlue => new(SKColors.MediumBlue); - public static Color MediumOrchid => new(SKColors.MediumOrchid); - public static Color MediumPurple => new(SKColors.MediumPurple); - public static Color MediumSeaGreen => new(SKColors.MediumSeaGreen); - public static Color MediumSlateBlue => new(SKColors.MediumSlateBlue); - public static Color MediumSpringGreen => new(SKColors.MediumSpringGreen); - public static Color MediumTurquoise => new(SKColors.MediumTurquoise); - public static Color MediumVioletRed => new(SKColors.MediumVioletRed); - public static Color MidnightBlue => new(SKColors.MidnightBlue); - public static Color MintCream => new(SKColors.MintCream); - public static Color MistyRose => new(SKColors.MistyRose); - public static Color Moccasin => new(SKColors.Moccasin); - public static Color NavajoWhite => new(SKColors.NavajoWhite); - public static Color Navy => new(SKColors.Navy); - public static Color OldLace => new(SKColors.OldLace); - public static Color Olive => new(SKColors.Olive); - public static Color OliveDrab => new(SKColors.OliveDrab); - public static Color Orange => new(SKColors.Orange); - public static Color OrangeRed => new(SKColors.OrangeRed); - public static Color Orchid => new(SKColors.Orchid); - public static Color PaleGoldenrod => new(SKColors.PaleGoldenrod); - public static Color PaleGreen => new(SKColors.PaleGreen); - public static Color PaleTurquoise => new(SKColors.PaleTurquoise); - public static Color PaleVioletRed => new(SKColors.PaleVioletRed); - public static Color PapayaWhip => new(SKColors.PapayaWhip); - public static Color PeachPuff => new(SKColors.PeachPuff); - public static Color Peru => new(SKColors.Peru); - public static Color Pink => new(SKColors.Pink); - public static Color Plum => new(SKColors.Plum); - public static Color PowderBlue => new(SKColors.PowderBlue); - public static Color Purple => new(SKColors.Purple); - public static Color Red => new(SKColors.Red); - public static Color RebeccaPurple => new("#663399"); - public static Color RosyBrown => new(SKColors.RosyBrown); - public static Color RoyalBlue => new(SKColors.RoyalBlue); - public static Color SaddleBrown => new(SKColors.SaddleBrown); - public static Color Salmon => new(SKColors.Salmon); - public static Color SandyBrown => new(SKColors.SandyBrown); - public static Color SeaGreen => new(SKColors.SeaGreen); - public static Color SeaShell => new(SKColors.SeaShell); - public static Color Sienna => new(SKColors.Sienna); - public static Color Silver => new(SKColors.Silver); - public static Color SkyBlue => new(SKColors.SkyBlue); - public static Color SlateBlue => new(SKColors.SlateBlue); - public static Color SlateGray => new(SKColors.SlateGray); - public static Color Snow => new(SKColors.Snow); - public static Color SpringGreen => new(SKColors.SpringGreen); - public static Color SteelBlue => new(SKColors.SteelBlue); - public static Color Tan => new(SKColors.Tan); - public static Color Teal => new(SKColors.Teal); - public static Color Thistle => new(SKColors.Thistle); - public static Color Tomato => new(SKColors.Tomato); - public static Color Turquoise => new(SKColors.Turquoise); - public static Color Violet => new(SKColors.Violet); - public static Color Wheat => new(SKColors.Wheat); - public static Color White => new(SKColors.White); - public static Color WhiteSmoke => new(SKColors.WhiteSmoke); - public static Color Yellow => new(SKColors.Yellow); - public static Color YellowGreen => new(SKColors.YellowGreen); + public static Color Transparent => new(SKColors.Transparent, KnownColor.Transparent); + public static Color AliceBlue => new(SKColors.AliceBlue, KnownColor.AliceBlue); + public static Color AntiqueWhite => new(SKColors.AntiqueWhite, KnownColor.AntiqueWhite); + public static Color Aqua => new(SKColors.Aqua, KnownColor.Aqua); + public static Color Aquamarine => new(SKColors.Aquamarine, KnownColor.Aquamarine); + public static Color Azure => new(SKColors.Azure, KnownColor.Azure); + public static Color Beige => new(SKColors.Beige, KnownColor.Beige); + public static Color Bisque => new(SKColors.Bisque, KnownColor.Bisque); + public static Color Black => new(SKColors.Black, KnownColor.Black); + public static Color BlanchedAlmond => new(SKColors.BlanchedAlmond, KnownColor.BlanchedAlmond); + public static Color Blue => new(SKColors.Blue, KnownColor.Blue); + public static Color BlueViolet => new(SKColors.BlueViolet, KnownColor.BlueViolet); + public static Color Brown => new(SKColors.Brown, KnownColor.Brown); + public static Color BurlyWood => new(SKColors.BurlyWood, KnownColor.BurlyWood); + public static Color CadetBlue => new(SKColors.CadetBlue, KnownColor.CadetBlue); + public static Color Chartreuse => new(SKColors.Chartreuse, KnownColor.Chartreuse); + public static Color Chocolate => new(SKColors.Chocolate, KnownColor.Chocolate); + public static Color Coral => new(SKColors.Coral, KnownColor.Coral); + public static Color CornflowerBlue => new(SKColors.CornflowerBlue, KnownColor.CornflowerBlue); + public static Color Cornsilk => new(SKColors.Cornsilk, KnownColor.Cornsilk); + public static Color Crimson => new(SKColors.Crimson, KnownColor.Crimson); + public static Color Cyan => new(SKColors.Cyan, KnownColor.Cyan); + public static Color DarkBlue => new(SKColors.DarkBlue, KnownColor.DarkBlue); + public static Color DarkCyan => new(SKColors.DarkCyan, KnownColor.DarkCyan); + public static Color DarkGoldenrod => new(SKColors.DarkGoldenrod, KnownColor.DarkGoldenrod); + public static Color DarkGray => new(SKColors.DarkGray, KnownColor.DarkGray); + public static Color DarkGreen => new(SKColors.DarkGreen, KnownColor.DarkGreen); + public static Color DarkKhaki => new(SKColors.DarkKhaki, KnownColor.DarkKhaki); + public static Color DarkMagenta => new(SKColors.DarkMagenta, KnownColor.DarkMagenta); + public static Color DarkOliveGreen => new(SKColors.DarkOliveGreen, KnownColor.DarkOliveGreen); + public static Color DarkOrange => new(SKColors.DarkOrange, KnownColor.DarkOrange); + public static Color DarkOrchid => new(SKColors.DarkOrchid, KnownColor.DarkOrchid); + public static Color DarkRed => new(SKColors.DarkRed, KnownColor.DarkRed); + public static Color DarkSalmon => new(SKColors.DarkSalmon, KnownColor.DarkSalmon); + public static Color DarkSeaGreen => new(SKColors.DarkSeaGreen, KnownColor.DarkSeaGreen); + public static Color DarkSlateBlue => new(SKColors.DarkSlateBlue, KnownColor.DarkSlateBlue); + public static Color DarkSlateGray => new(SKColors.DarkSlateGray, KnownColor.DarkSlateGray); + public static Color DarkTurquoise => new(SKColors.DarkTurquoise, KnownColor.DarkTurquoise); + public static Color DarkViolet => new(SKColors.DarkViolet, KnownColor.DarkViolet); + public static Color DeepPink => new(SKColors.DeepPink, KnownColor.DeepPink); + public static Color DeepSkyBlue => new(SKColors.DeepSkyBlue, KnownColor.DeepSkyBlue); + public static Color DimGray => new(SKColors.DimGray, KnownColor.DimGray); + public static Color DodgerBlue => new(SKColors.DodgerBlue, KnownColor.DodgerBlue); + public static Color Firebrick => new(SKColors.Firebrick, KnownColor.Firebrick); + public static Color FloralWhite => new(SKColors.FloralWhite, KnownColor.FloralWhite); + public static Color ForestGreen => new(SKColors.ForestGreen, KnownColor.ForestGreen); + public static Color Fuchsia => new(SKColors.Fuchsia, KnownColor.Fuchsia); + public static Color Gainsboro => new(SKColors.Gainsboro, KnownColor.Gainsboro); + public static Color GhostWhite => new(SKColors.GhostWhite, KnownColor.GhostWhite); + public static Color Gold => new(SKColors.Gold, KnownColor.Gold); + public static Color Goldenrod => new(SKColors.Goldenrod, KnownColor.Goldenrod); + public static Color Gray => new(SKColors.Gray, KnownColor.Gray); + public static Color Green => new(SKColors.Green, KnownColor.Green); + public static Color GreenYellow => new(SKColors.GreenYellow, KnownColor.GreenYellow); + public static Color Honeydew => new(SKColors.Honeydew, KnownColor.Honeydew); + public static Color HotPink => new(SKColors.HotPink, KnownColor.HotPink); + public static Color IndianRed => new(SKColors.IndianRed, KnownColor.IndianRed); + public static Color Indigo => new(SKColors.Indigo, KnownColor.Indigo); + public static Color Ivory => new(SKColors.Ivory, KnownColor.Ivory); + public static Color Khaki => new(SKColors.Khaki, KnownColor.Khaki); + public static Color Lavender => new(SKColors.Lavender, KnownColor.Lavender); + public static Color LavenderBlush => new(SKColors.LavenderBlush, KnownColor.LavenderBlush); + public static Color LawnGreen => new(SKColors.LawnGreen, KnownColor.LawnGreen); + public static Color LemonChiffon => new(SKColors.LemonChiffon, KnownColor.LemonChiffon); + public static Color LightBlue => new(SKColors.LightBlue, KnownColor.LightBlue); + public static Color LightCoral => new(SKColors.LightCoral, KnownColor.LightCoral); + public static Color LightCyan => new(SKColors.LightCyan, KnownColor.LightCyan); + public static Color LightGoldenrodYellow => new(SKColors.LightGoldenrodYellow, KnownColor.LightGoldenrodYellow); + public static Color LightGray => new(SKColors.LightGray, KnownColor.LightGray); + public static Color LightGreen => new(SKColors.LightGreen, KnownColor.LightGreen); + public static Color LightPink => new(SKColors.LightPink, KnownColor.LightPink); + public static Color LightSalmon => new(SKColors.LightSalmon, KnownColor.LightSalmon); + public static Color LightSeaGreen => new(SKColors.LightSeaGreen, KnownColor.LightSeaGreen); + public static Color LightSkyBlue => new(SKColors.LightSkyBlue, KnownColor.LightSkyBlue); + public static Color LightSlateGray => new(SKColors.LightSlateGray, KnownColor.LightSlateGray); + public static Color LightSteelBlue => new(SKColors.LightSteelBlue, KnownColor.LightSteelBlue); + public static Color LightYellow => new(SKColors.LightYellow, KnownColor.LightYellow); + public static Color Lime => new(SKColors.Lime, KnownColor.Lime); + public static Color LimeGreen => new(SKColors.LimeGreen, KnownColor.LimeGreen); + public static Color Linen => new(SKColors.Linen, KnownColor.Linen); + public static Color Magenta => new(SKColors.Magenta, KnownColor.Magenta); + public static Color Maroon => new(SKColors.Maroon, KnownColor.Maroon); + public static Color MediumAquamarine => new(SKColors.MediumAquamarine, KnownColor.MediumAquamarine); + public static Color MediumBlue => new(SKColors.MediumBlue, KnownColor.MediumBlue); + public static Color MediumOrchid => new(SKColors.MediumOrchid, KnownColor.MediumOrchid); + public static Color MediumPurple => new(SKColors.MediumPurple, KnownColor.MediumPurple); + public static Color MediumSeaGreen => new(SKColors.MediumSeaGreen, KnownColor.MediumSeaGreen); + public static Color MediumSlateBlue => new(SKColors.MediumSlateBlue, KnownColor.MediumSlateBlue); + public static Color MediumSpringGreen => new(SKColors.MediumSpringGreen, KnownColor.MediumSpringGreen); + public static Color MediumTurquoise => new(SKColors.MediumTurquoise, KnownColor.MediumTurquoise); + public static Color MediumVioletRed => new(SKColors.MediumVioletRed, KnownColor.MediumVioletRed); + public static Color MidnightBlue => new(SKColors.MidnightBlue, KnownColor.MidnightBlue); + public static Color MintCream => new(SKColors.MintCream, KnownColor.MintCream); + public static Color MistyRose => new(SKColors.MistyRose, KnownColor.MistyRose); + public static Color Moccasin => new(SKColors.Moccasin, KnownColor.Moccasin); + public static Color NavajoWhite => new(SKColors.NavajoWhite, KnownColor.NavajoWhite); + public static Color Navy => new(SKColors.Navy, KnownColor.Navy); + public static Color OldLace => new(SKColors.OldLace, KnownColor.OldLace); + public static Color Olive => new(SKColors.Olive, KnownColor.Olive); + public static Color OliveDrab => new(SKColors.OliveDrab, KnownColor.OliveDrab); + public static Color Orange => new(SKColors.Orange, KnownColor.Orange); + public static Color OrangeRed => new(SKColors.OrangeRed, KnownColor.OrangeRed); + public static Color Orchid => new(SKColors.Orchid, KnownColor.Orchid); + public static Color PaleGoldenrod => new(SKColors.PaleGoldenrod, KnownColor.PaleGoldenrod); + public static Color PaleGreen => new(SKColors.PaleGreen, KnownColor.PaleGreen); + public static Color PaleTurquoise => new(SKColors.PaleTurquoise, KnownColor.PaleTurquoise); + public static Color PaleVioletRed => new(SKColors.PaleVioletRed, KnownColor.PaleVioletRed); + public static Color PapayaWhip => new(SKColors.PapayaWhip, KnownColor.PapayaWhip); + public static Color PeachPuff => new(SKColors.PeachPuff, KnownColor.PeachPuff); + public static Color Peru => new(SKColors.Peru, KnownColor.Peru); + public static Color Pink => new(SKColors.Pink, KnownColor.Pink); + public static Color Plum => new(SKColors.Plum, KnownColor.Plum); + public static Color PowderBlue => new(SKColors.PowderBlue, KnownColor.PowderBlue); + public static Color Purple => new(SKColors.Purple, KnownColor.Purple); + public static Color Red => new(SKColors.Red, KnownColor.Red); + public static Color RebeccaPurple => new(SKColor.Parse("#663399"), KnownColor.RebeccaPurple); + public static Color RosyBrown => new(SKColors.RosyBrown, KnownColor.RosyBrown); + public static Color RoyalBlue => new(SKColors.RoyalBlue, KnownColor.RoyalBlue); + public static Color SaddleBrown => new(SKColors.SaddleBrown, KnownColor.SaddleBrown); + public static Color Salmon => new(SKColors.Salmon, KnownColor.Salmon); + public static Color SandyBrown => new(SKColors.SandyBrown, KnownColor.SandyBrown); + public static Color SeaGreen => new(SKColors.SeaGreen, KnownColor.SeaGreen); + public static Color SeaShell => new(SKColors.SeaShell, KnownColor.SeaShell); + public static Color Sienna => new(SKColors.Sienna, KnownColor.Sienna); + public static Color Silver => new(SKColors.Silver, KnownColor.Silver); + public static Color SkyBlue => new(SKColors.SkyBlue, KnownColor.SkyBlue); + public static Color SlateBlue => new(SKColors.SlateBlue, KnownColor.SlateBlue); + public static Color SlateGray => new(SKColors.SlateGray, KnownColor.SlateGray); + public static Color Snow => new(SKColors.Snow, KnownColor.Snow); + public static Color SpringGreen => new(SKColors.SpringGreen, KnownColor.SpringGreen); + public static Color SteelBlue => new(SKColors.SteelBlue, KnownColor.SteelBlue); + public static Color Tan => new(SKColors.Tan, KnownColor.Tan); + public static Color Teal => new(SKColors.Teal, KnownColor.Teal); + public static Color Thistle => new(SKColors.Thistle, KnownColor.Thistle); + public static Color Tomato => new(SKColors.Tomato, KnownColor.Tomato); + public static Color Turquoise => new(SKColors.Turquoise, KnownColor.Turquoise); + public static Color Violet => new(SKColors.Violet, KnownColor.Violet); + public static Color Wheat => new(SKColors.Wheat, KnownColor.Wheat); + public static Color White => new(SKColors.White, KnownColor.White); + public static Color WhiteSmoke => new(SKColors.WhiteSmoke, KnownColor.WhiteSmoke); + public static Color Yellow => new(SKColors.Yellow, KnownColor.Yellow); + public static Color YellowGreen => new(SKColors.YellowGreen, KnownColor.YellowGreen); #endregion } From e8a9c9ea878a197f77fd937ca1de20bfbde54d8d Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 12:12:14 -0300 Subject: [PATCH 199/217] minor fix in description of ColorTranslator class --- src/Common/ColorTranslator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/ColorTranslator.cs b/src/Common/ColorTranslator.cs index 0777623..020d81a 100644 --- a/src/Common/ColorTranslator.cs +++ b/src/Common/ColorTranslator.cs @@ -4,7 +4,7 @@ namespace GeneXus.Drawing; /// -/// Translates colors to and from GDI+ objects. +/// Translates colors to and from objects. /// public static class ColorTranslator { From a1da1669bac69f98f842459beebe4a978525af94 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 12:12:36 -0300 Subject: [PATCH 200/217] add ConvertFromString method in ColorTranslator class --- src/Common/ColorTranslator.cs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Common/ColorTranslator.cs b/src/Common/ColorTranslator.cs index 020d81a..ba859bf 100644 --- a/src/Common/ColorTranslator.cs +++ b/src/Common/ColorTranslator.cs @@ -36,19 +36,7 @@ public static int ToWin32(Color c) /// Translates an Html color representation to a . /// public static Color FromHtml(string htmlColor) - { - if (htmlColor.StartsWith("#")) - return Color.FromHex(htmlColor); - - if (htmlColor.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || htmlColor.StartsWith("&h", StringComparison.OrdinalIgnoreCase)) - return Color.FromArgb(Convert.ToInt32(htmlColor.Substring(2), 16)); - - var provider = (NumberFormatInfo)CultureInfo.CurrentCulture.GetFormat(typeof(NumberFormatInfo)); - if (int.TryParse(htmlColor, NumberStyles.Integer, provider, out int value)) - return Color.FromArgb(value); - - return Color.FromName(htmlColor); - } + => ConvertFromString(htmlColor, CultureInfo.CurrentCulture); /// /// Translates an Ole color value to a . @@ -65,7 +53,7 @@ public static Color FromWin32(int win32Color) #endregion - #region Helpers + #region Utilities private static uint RefToArgb(uint value) => ((value >> R_SHIFT) & 0xFF) << 16 @@ -73,5 +61,20 @@ private static uint RefToArgb(uint value) | ((value >> B_SHIFT) & 0xFF) << 0 | 0xFFu << 24; + internal static Color ConvertFromString(string value, CultureInfo culture) + { + if (value.StartsWith("#")) + return Color.FromHex(value); + + if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || value.StartsWith("&h", StringComparison.OrdinalIgnoreCase)) + return Color.FromArgb(Convert.ToInt32(value.Substring(2), 16)); + + var provider = (NumberFormatInfo)CultureInfo.CurrentCulture.GetFormat(typeof(NumberFormatInfo)); + if (int.TryParse(value, NumberStyles.Integer, provider, out int argb)) + return Color.FromArgb(argb); + + return Color.FromName(value.Trim('\'')); + } + #endregion } \ No newline at end of file From 8324182892b3156f1a774bcf06dbd70f8446e4ed Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 12:13:26 -0300 Subject: [PATCH 201/217] add ColorConverter class --- src/Common/ColorConverter.cs | 142 +++++++++++++++++++++++++ test/Common/ColorConverterUnitTests.cs | 100 +++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 src/Common/ColorConverter.cs create mode 100644 test/Common/ColorConverterUnitTests.cs diff --git a/src/Common/ColorConverter.cs b/src/Common/ColorConverter.cs new file mode 100644 index 0000000..5455874 --- /dev/null +++ b/src/Common/ColorConverter.cs @@ -0,0 +1,142 @@ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Design.Serialization; +using System.Globalization; +using System.Linq; +using System.Reflection; + +namespace GeneXus.Drawing; + +public class ColorConverter : TypeConverter +{ + private static readonly Lazy s_valuesLazy = new(() => + { + var set = new HashSet(); + foreach (PropertyInfo prop in typeof(Color).GetProperties(BindingFlags.Public | BindingFlags.Static)) + if (prop.GetValue(null) is Color color) + set.Add(color); + return new StandardValuesCollection(set.OrderBy(c => c, new ColorComparer()).ToList()); + }); + + /// + /// Initializes a new instance of the class. + /// + public ColorConverter() { } + + + # region TypeConverter overrides + + /// + /// Determines if this converter can convert an object in the given source type to the native type of the converter. + /// + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + + /// + /// Returns a value indicating whether this converter can convert an object to the given destination type using the context. + /// + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + => destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType); + + /// + /// Converts the given object to the converter's native type. + /// + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + => value is string strValue ? ColorTranslator.ConvertFromString(strValue, culture ?? CultureInfo.CurrentCulture) + : base.ConvertFrom(context, culture, value); + + /// + /// Converts the specified object to another type. + /// + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == null) + throw new ArgumentNullException(nameof(destinationType)); + + if (value is Color c) + { + if (destinationType == typeof(string)) + { + if (c.IsEmpty) + return string.Empty; + + if (c.IsKnownColor) + return c.Name; + + if (c.IsNamedColor) + return $"'{c.Name}'"; + + culture ??= CultureInfo.CurrentCulture; + + var converter = TypeDescriptor.GetConverter(typeof(int)); + var components = c.A < 255 ? new[] { c.A, c.R, c.G, c.B } : new[] { c.R, c.G, c.B }; + + var sep = string.Concat(culture.TextInfo.ListSeparator, " "); + var args = components.Select(comp => converter.ConvertToString(context, culture, comp)); + return string.Join(sep, args); + } + else if (destinationType == typeof(InstanceDescriptor)) + { + MemberInfo member = null; + object[] args = new object[] { }; + + if (c.IsEmpty) + { + member = typeof(Color).GetField("Empty"); + } + else if (c.IsKnownColor) + { + member = typeof(Color).GetProperty(c.Name); + } + else if (c.IsNamedColor) + { + member = typeof(Color).GetMethod("FromName", new[] { typeof(string) }); + args = new object[] { c.Name }; + } + else if (c.A < 255) + { + member = typeof(Color).GetMethod("FromArgb", new[] { typeof(int), typeof(int), typeof(int), typeof(int) }); + args = new object[] { c.A, c.R, c.G, c.B }; + } + else + { + member = typeof(Color).GetMethod("FromArgb", new[] { typeof(int), typeof(int), typeof(int) }); + args = new object[] { c.R, c.G, c.B }; + } + + if (member != null) + return new InstanceDescriptor(member, args); + return null; + } + } + + return base.ConvertTo(context, culture, value, destinationType); + } + + /// + /// Retrieves a collection containing a set of standard values for the data type for which this validator + /// is designed. This will return null if the data type does not support a standard set of values. + /// + public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) + => s_valuesLazy.Value; + + /// + /// Determines if this object supports a standard set of values that can be chosen from a list. + /// + public override bool GetStandardValuesSupported(ITypeDescriptorContext context) + => true; + + #endregion + + + #region Utilities + + private sealed class ColorComparer : IComparer + { + public int Compare(Color left, Color right) => string.CompareOrdinal(left.Name, right.Name); + } + + #endregion +} \ No newline at end of file diff --git a/test/Common/ColorConverterUnitTests.cs b/test/Common/ColorConverterUnitTests.cs new file mode 100644 index 0000000..5c3478c --- /dev/null +++ b/test/Common/ColorConverterUnitTests.cs @@ -0,0 +1,100 @@ +namespace GeneXus.Drawing.Test; + +internal class ColorConverterTests +{ + private ColorConverter converter; + + [SetUp] + public void SetUp() + { + converter = new ColorConverter(); + } + + [Test] + public void Method_CanConvertFrom_String() + { + Assert.That(converter.CanConvertFrom(typeof(string)), Is.True); + } + + [Test] + public void Method_CanConvertFrom_Color() + { + Assert.That(converter.CanConvertFrom(typeof(Color)), Is.False); + } + + [Test] + public void Method_CanConvertTo_String() + { + Assert.That(converter.CanConvertTo(typeof(string)), Is.True); + } + + [Test] + public void Method_CanConvertTo_Int() + { + Assert.That(converter.CanConvertTo(typeof(int)), Is.False); + } + + [Test] + public void Method_ConvertFrom_ColorName() + { + var result = converter.ConvertFrom("Red"); + + Assert.That(result, Is.EqualTo(Color.Red)); + } + + [Test] + public void Method_ConvertFrom_HexString() + { + var result = converter.ConvertFrom("#FF0000"); + + Assert.That(result, Is.EqualTo(Color.Red)); + } + + [Test] + public void Method_ConvertFrom_Empty() + { + var result = converter.ConvertFrom(""); + + Assert.That(result, Is.EqualTo(Color.Empty)); + } + + [Test] + public void Method_ConvertTo_String_Named() + { + var result = converter.ConvertTo(Color.Blue, typeof(string)); + + Assert.That(result, Is.EqualTo("Blue")); + } + + [Test] + public void Method_ConvertTo_String_Empty() + { + var result = converter.ConvertTo(Color.Empty, typeof(string)); + + Assert.That(result, Is.EqualTo(string.Empty)); + } + + [Test] + public void Method_ConvertFromInvariantString_Named() + { + var result = converter.ConvertFromInvariantString("Green"); + + Assert.That(result, Is.EqualTo(Color.Green)); + } + + [Test] + public void Method_ConvertFromInvariantString_Hex() + { + var result = converter.ConvertFromInvariantString("#00FF00"); + + Assert.That(result, Is.EqualTo(Color.Lime)); + } + + [Test] + public void Method_ConvertFromInvariantString_Empty() + { + var result = converter.ConvertFromInvariantString(""); + + Assert.That(result, Is.EqualTo(Color.Empty)); + } +} \ No newline at end of file From d742aa3559d01b295aa10a22d6f12c16affb85be Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 12:14:52 -0300 Subject: [PATCH 202/217] update ColorTranslator unit test after fixing known colors for Color class --- test/Common/ColorTranslatorUnitTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Common/ColorTranslatorUnitTest.cs b/test/Common/ColorTranslatorUnitTest.cs index 7eab790..0ee42ad 100644 --- a/test/Common/ColorTranslatorUnitTest.cs +++ b/test/Common/ColorTranslatorUnitTest.cs @@ -13,7 +13,7 @@ public void Method_ToHtml() var color = Color.Red; var htmlColor = ColorTranslator.ToHtml(color); - Assert.That(htmlColor, Is.EqualTo("#F00")); + Assert.That(htmlColor, Is.EqualTo("Red")); } [Test] From e4b1b384d2b38fb7f86fc4209c7aebba59faeed2 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 12:17:42 -0300 Subject: [PATCH 203/217] add summary for ColorConverter class and update README --- README.md | 1 + src/Common/ColorConverter.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index a4fd3e6..8c83bcb 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Basic graphics funcionalities based on `System.Drawing`. | `Bitmap` | Class | Represents an image defined by pixels. | `Brush` | Class | Abstract class for brushes used to fill graphics shapes. | `Color` | Class | Defines colors used for drawing. +| `ColorConverter` | Class | Converts colors from one data type to another | `ColorTranslator` | Class | Translates colors to and from HTML, OLE and Win32 representations. | `Font` | Class | Defines a format for text, including font family, size, and style. | `Graphics` | Class | Provides methods for drawing on a drawing surface. diff --git a/src/Common/ColorConverter.cs b/src/Common/ColorConverter.cs index 5455874..53b8934 100644 --- a/src/Common/ColorConverter.cs +++ b/src/Common/ColorConverter.cs @@ -9,6 +9,9 @@ namespace GeneXus.Drawing; +/// +/// Converts colors from one data type to another. Access this class through the . +/// public class ColorConverter : TypeConverter { private static readonly Lazy s_valuesLazy = new(() => From b54e031bb2891e7294d9432cdbcf3ae08fbcc2e1 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 13:31:01 -0300 Subject: [PATCH 204/217] fix AddString typeface disposed variable --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index e34f271..4baeb5e 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -918,7 +918,7 @@ private void AddString(string text, FontFamily family, int style, float emSize, bool isRightToLeft = format.FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft); - using var typeface = family.GetTypeface((FontStyle)style); + var typeface = family.GetTypeface((FontStyle)style); using var font = new SKFont(typeface, (int)emSize); using var paint = new SKPaint(font) From e8649b76adaec4f451781329c09caa10bbcfca05 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 14:05:01 -0300 Subject: [PATCH 205/217] minor change --- src/Common/Drawing2D/GraphicsPath.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 4baeb5e..529eb1f 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -141,7 +141,7 @@ public byte[] PathTypes var points = new SKPoint[4]; SKPathVerb verb; - while ((verb = iterator.Next(points))!= SKPathVerb.Done) + while ((verb = iterator.Next(points)) != SKPathVerb.Done) { switch (verb) { From 7238c05ab0695ee58ee0022e608c091911ff85b9 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 14:52:29 -0300 Subject: [PATCH 206/217] add note for Graphics.DrawString method that uses path drawing instead of skia's DrawText method; as a result, the output may experience a slight offset in positioning and measuring --- src/Common/Graphics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index a0b360d..afc9b03 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -874,7 +874,7 @@ public void DrawString(string text, Font font, Brush brush, RectangleF layout, S using var path = new GraphicsPath(); path.AddString(text, font.FontFamily, (int)font.Style, emSize, layout, format); - FillPath(brush, path); + FillPath(brush, path); // NOTE: path has points defined by float values, but skia does not support subpixel path drawing } /// From e76ef8b4f654b36e98fb8a4fda0cbd6e5107179b Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 14:54:01 -0300 Subject: [PATCH 207/217] add Graphics.GetStringBounds private method to be used by MeasureCharacterRanges and MeasureStringInternal public methods --- src/Common/Graphics.cs | 44 ++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index afc9b03..a0e56bf 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -1446,18 +1446,10 @@ public Region[] MeasureCharacterRanges(ReadOnlySpan text, Font font, Recta /// public Region[] MeasureCharacterRanges(string text, Font font, RectangleF layout, StringFormat format) { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (font == null) throw new ArgumentNullException(nameof(font)); - - float emSize = DpiY * GetFactor(font.Size, font.Unit, GraphicsUnit.Pixel); - var regions = new List(); foreach (var substring in format.ApplyRanges(text)) { - using var path = new GraphicsPath(); - path.AddString(substring, font.FontFamily, (int)font.Style, emSize, layout, format); - - var bounds = path.GetBounds(); + var bounds = GetStringBounds(substring, font, layout, format); var region = new Region(bounds); regions.Add(region); } @@ -1558,19 +1550,14 @@ public SizeF MeasureStringInternal(ReadOnlySpan text, Font font, Rectangle /// public SizeF MeasureStringInternal(string text, Font font, RectangleF layoutArea, StringFormat format, out int charsFitted, out int linesFilled) { - float emSize = DpiY * GetFactor(font.Size, font.Unit, GraphicsUnit.Pixel); - - using var path = new GraphicsPath(); - path.AddString(text, font.FontFamily, (int)font.Style, emSize, layoutArea, format); - - var bounds = path.GetBounds(); + var bounds = GetStringBounds(text, font, layoutArea, format); var lines = text.Split(StringFormat.BREAKLINES, StringSplitOptions.None); - float totalWidth = bounds.Width + 2 * bounds.Left; + float totalWidth = bounds.Width; float charWidth = totalWidth / lines.Max(line => line.Length); charsFitted = (int)(totalWidth / charWidth); - float totalHeight = bounds.Height + 2 * bounds.Top; + float totalHeight = bounds.Height; float lineHeight = totalHeight / lines.Length; linesFilled = (int)(totalHeight / lineHeight); @@ -1888,6 +1875,29 @@ private static GraphicsPath GetRectanglePath(RectangleF rect) return path; } + private RectangleF GetStringBounds(string text, Font font, RectangleF layout, StringFormat format) + { + /* NOTE: Skia's MeasureText is not used because of StringFormat rendering, that's why + * we use GraphicsPath that already supports StringFormat class definition + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + if (text == null) throw new ArgumentNullException(nameof(text)); + if (font == null) throw new ArgumentNullException(nameof(font)); + + float emSize = DpiY * GetFactor(font.Size, font.Unit, GraphicsUnit.Pixel); + + using var path = new GraphicsPath(); + path.AddString(text, font.FontFamily, (int)font.Style, emSize, layout, format); + + var bounds = path.GetBounds(); + + // NOTE: the Y value is always 0 in System.Drawing, so that distance is added to height twice (for top and bottom) + bounds.Height += 2 * bounds.Y; + bounds.Y = 0; + + return bounds; + } + private void TransformPoints(CoordinateSpace destination, CoordinateSpace source, T[] points, Func getPoint, Func newPoint) { if (source == destination) From 9558d904b567933e66997e3de9d58e842f129540 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 14:54:57 -0300 Subject: [PATCH 208/217] add ceiling in GraphicsPath.AddString method in order to skia can render the underline when the rectangle is defined by floating values --- src/Common/Drawing2D/GraphicsPath.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 529eb1f..67ce580 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -982,6 +982,7 @@ private void AddString(string text, FontFamily family, int style, float emSize, underlineTop, origin + length + rtlOffsetX, underlineTop + underlineHeight); + underline = SKRectI.Ceiling(underline); textPath.AddRect(underline); } From 66dbf9e6250602493235a9ef7a0f3bd119a40522 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 14:55:57 -0300 Subject: [PATCH 209/217] minor change in StringFormat.ApplyWrapping and StringFormat.ApplyTrimming method when deciding split text in a new line --- src/Common/StringFormat.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/StringFormat.cs b/src/Common/StringFormat.cs index 25a1e44..1b97791 100644 --- a/src/Common/StringFormat.cs +++ b/src/Common/StringFormat.cs @@ -392,7 +392,7 @@ internal string ApplyWrapping(string text, SKRect boundBox, Func foreach (var word in line.Split(' ')) { var curWidth = measureText(word); - if (Math.Ceiling(accWidth += curWidth) >= boundBox.Width) + if (Math.Round(accWidth += curWidth) > boundBox.Width) { sb.Append('\n').Append(' '); accWidth = curWidth; @@ -429,7 +429,7 @@ private string ApplyTrimming(IEnumerable tokens, string endToken, SKRect } } - if (Math.Ceiling((accWidth += curWidth) + endLen) >= boundBox.Width) + if (Math.Round((accWidth += curWidth) + endLen) > boundBox.Width) { if (FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft)) sb.Insert(0, endToken); From 664085624dabf6222c1521caa02e857ea21ddcbb Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 14:56:19 -0300 Subject: [PATCH 210/217] add CompareImage to Utils class for comparing images --- test/Common/Utils.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/Common/Utils.cs b/test/Common/Utils.cs index 0d135ce..d39b4b0 100644 --- a/test/Common/Utils.cs +++ b/test/Common/Utils.cs @@ -99,6 +99,15 @@ public static float CompareImage(string filename, Brush brush, bool save = false using var g = Graphics.FromImage(bg); g.FillRectangle(brush, bg.GetBounds(ref gu)); + return CompareImage(filename, bg, save); + } + + public static float CompareImage(string filename, Bitmap bg, bool save = false) + { + string filepath = Path.Combine(IMAGE_PATH, filename); + using var im = Image.FromFile(filepath); + using var bm = new Bitmap(im); + if (save) { string savepath = Path.Combine(IMAGE_PATH, ".out", filename); From ac9bbfc949ed44255303bd0fb1d531021d634216 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 14:57:18 -0300 Subject: [PATCH 211/217] add notes in Graphics class for PixelOffsetMode, TextContrast and TextRenderingHint properties --- src/Common/Graphics.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs index a0e56bf..64544c3 100644 --- a/src/Common/Graphics.cs +++ b/src/Common/Graphics.cs @@ -143,7 +143,7 @@ public Region Clip /// /// Gets or sets a value specifying how pixels are offset during rendering of this . /// - public PixelOffsetMode PixelOffsetMode { get; set; } = PixelOffsetMode.Default; + public PixelOffsetMode PixelOffsetMode { get; set; } = PixelOffsetMode.Default; // TODO: to be implemented /// /// Gets or sets the rendering origin of this for dithering and for hatch brushes. @@ -158,12 +158,12 @@ public Region Clip /// /// Gets or sets the gamma correction value for rendering text. /// - public int TextContrast { get; set; } = 4; + public int TextContrast { get; set; } = 4; // TODO: to be implemented /// /// Gets or sets the rendering mode for text associated with this . /// - public TextRenderingHint TextRenderingHint { get; set; } = TextRenderingHint.SystemDefault; + public TextRenderingHint TextRenderingHint { get; set; } = TextRenderingHint.SystemDefault; // TODO: to be implemented /// /// Gets or sets a copy of the geometric world transformation for this . From 9904c00eb88feec9d2899c613911b1fee80d0ed0 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 14:57:55 -0300 Subject: [PATCH 212/217] add unit tests for Graphics class --- test/Common/GraphicsUnitTest.cs | 928 ++++++++++++++++++ .../res/images/graphics/ClipExclude.png | Bin 0 -> 310 bytes .../res/images/graphics/ClipIntersect.png | Bin 0 -> 227 bytes .../res/images/graphics/ClipTranslate.png | Bin 0 -> 256 bytes test/Common/res/images/graphics/DrawArc.png | Bin 0 -> 163 bytes .../Common/res/images/graphics/DrawBezier.png | Bin 0 -> 189 bytes .../res/images/graphics/DrawClosedCurve.png | Bin 0 -> 288 bytes test/Common/res/images/graphics/DrawCurve.png | Bin 0 -> 252 bytes .../res/images/graphics/DrawEllipse.png | Bin 0 -> 269 bytes test/Common/res/images/graphics/DrawImage.png | Bin 0 -> 1025 bytes test/Common/res/images/graphics/DrawLine.png | Bin 0 -> 174 bytes test/Common/res/images/graphics/DrawPath.png | Bin 0 -> 266 bytes test/Common/res/images/graphics/DrawPie.png | Bin 0 -> 206 bytes .../res/images/graphics/DrawPolygon.png | Bin 0 -> 387 bytes .../res/images/graphics/DrawRectangle.png | Bin 0 -> 199 bytes .../Common/res/images/graphics/DrawString.png | Bin 0 -> 403 bytes .../res/images/graphics/FillClosedCurve.png | Bin 0 -> 308 bytes .../res/images/graphics/FillEllipse.png | Bin 0 -> 301 bytes test/Common/res/images/graphics/FillPath.png | Bin 0 -> 292 bytes test/Common/res/images/graphics/FillPie.png | Bin 0 -> 212 bytes .../res/images/graphics/FillPolygon.png | Bin 0 -> 477 bytes .../res/images/graphics/FillRectangle.png | Bin 0 -> 284 bytes .../Common/res/images/graphics/FillRegion.png | Bin 0 -> 282 bytes .../images/graphics/RenderingOrigin_00-00.png | Bin 0 -> 507 bytes .../images/graphics/RenderingOrigin_25-25.png | Bin 0 -> 503 bytes 25 files changed, 928 insertions(+) create mode 100644 test/Common/GraphicsUnitTest.cs create mode 100644 test/Common/res/images/graphics/ClipExclude.png create mode 100644 test/Common/res/images/graphics/ClipIntersect.png create mode 100644 test/Common/res/images/graphics/ClipTranslate.png create mode 100644 test/Common/res/images/graphics/DrawArc.png create mode 100644 test/Common/res/images/graphics/DrawBezier.png create mode 100644 test/Common/res/images/graphics/DrawClosedCurve.png create mode 100644 test/Common/res/images/graphics/DrawCurve.png create mode 100644 test/Common/res/images/graphics/DrawEllipse.png create mode 100644 test/Common/res/images/graphics/DrawImage.png create mode 100644 test/Common/res/images/graphics/DrawLine.png create mode 100644 test/Common/res/images/graphics/DrawPath.png create mode 100644 test/Common/res/images/graphics/DrawPie.png create mode 100644 test/Common/res/images/graphics/DrawPolygon.png create mode 100644 test/Common/res/images/graphics/DrawRectangle.png create mode 100644 test/Common/res/images/graphics/DrawString.png create mode 100644 test/Common/res/images/graphics/FillClosedCurve.png create mode 100644 test/Common/res/images/graphics/FillEllipse.png create mode 100644 test/Common/res/images/graphics/FillPath.png create mode 100644 test/Common/res/images/graphics/FillPie.png create mode 100644 test/Common/res/images/graphics/FillPolygon.png create mode 100644 test/Common/res/images/graphics/FillRectangle.png create mode 100644 test/Common/res/images/graphics/FillRegion.png create mode 100644 test/Common/res/images/graphics/RenderingOrigin_00-00.png create mode 100644 test/Common/res/images/graphics/RenderingOrigin_25-25.png diff --git a/test/Common/GraphicsUnitTest.cs b/test/Common/GraphicsUnitTest.cs new file mode 100644 index 0000000..57aefc9 --- /dev/null +++ b/test/Common/GraphicsUnitTest.cs @@ -0,0 +1,928 @@ +using System; +using System.Diagnostics; +using System.IO; +using GeneXus.Drawing.Drawing2D; +using GeneXus.Drawing.Text; + +namespace GeneXus.Drawing.Test; + +internal class GraphicsUnitTest +{ + private static readonly string IMAGE_PATH = Path.Combine( + Directory.GetParent(Environment.CurrentDirectory).Parent.FullName, + "res", "images"); + + private static readonly string FONT_PATH = Path.Combine( + Directory.GetParent(Environment.CurrentDirectory).Parent.FullName, + "res", "fonts"); + + [SetUp] + public void Setup() + { + } + + + #region Factory methods + + [Test] + public void Method_FromImage() + { + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + Assert.That(bitmap.GetPixel(25, 25), Is.EqualTo(Color.Empty)); + } + + #endregion + + + #region Propeties + + [Test] + [TestCase(CompositingMode.SourceCopy, "#FF000080")] + [TestCase(CompositingMode.SourceOver, "#80007FFF")] + public void Property_CompositingMode(CompositingMode mode, string hex) + { + var color = Color.FromArgb(128, 255, 0, 0); + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.Clear(Color.Blue); + g.CompositingMode = mode; + g.FillRectangle(brush, 10, 10, 30, 30); + + Assert.Multiple(() => + { + var expected = Color.FromHex(hex); + Assert.That(bitmap.GetPixel(0, 0), Is.EqualTo(Color.Blue)); + Assert.That(bitmap.GetPixel(25, 25), Is.EqualTo(expected)); + }); + } + + [Test] + [TestCase(0, 0)] + [TestCase(25, 25)] + public void Property_RenderingOrigin(int x, int y) + { + using var brush = new HatchBrush(HatchStyle.DiagonalCross, Color.Green, Color.Orange); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.Clear(Color.Blue); + g.RenderingOrigin = new Point(x, y); + g.FillRectangle(brush, 10, 10, 30, 30); + + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"RenderingOrigin_{x:D2}-{y:D2}.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + #endregion + + + #region Utilities methods + + [Test] + public void Method_Clear() + { + var color = Color.Red; + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.Clear(color); + Assert.Multiple(() => + { + for (int i = 0; i < bitmap.Width; i++) + for (int j = 0; j < bitmap.Height; j++) + Assert.That(bitmap.GetPixel(i, j), Is.EqualTo(color)); + }); + } + + [Test] + public void Method_CopyFromScreen() + { + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var size = new Size(25, 25); + Assert.Throws(() => + { + g.CopyFromScreen(0, 0, bitmap.Width, bitmap.Height, size); + }); + } + + [Test] + public void Method_GetNearestColor() + { + var color = Color.FromArgb(123, 200, 123); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var nearest = g.GetNearestColor(color); + Assert.Multiple(() => + { + Assert.That(nearest.R, Is.EqualTo(color.R).Within(5)); + Assert.That(nearest.G, Is.EqualTo(color.G).Within(5)); + Assert.That(nearest.B, Is.EqualTo(color.B).Within(5)); + }); + } + + [Test] + public void Method_IsVisible() + { + var color = Color.Red; + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.FillRectangle(brush, 10, 10, 30, 30); + Assert.Multiple(() => + { + Assert.That(g.IsVisible(25, 25), Is.True); + Assert.That(g.IsVisible(50, 50), Is.False); + }); + } + + #endregion + + + #region State methods + + [Test] + public void Method_SaveAndRestore() + { + // NOTE: Save/Restore only works over transform matrix in both System.Drawing and SkiaSharp + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.TranslateTransform(10, 10); + + var state = g.Save(); + Assert.Multiple(() => + { + Assert.That(g.Transform.OffsetX, Is.EqualTo(10)); + Assert.That(g.Transform.OffsetY, Is.EqualTo(10)); + }); + + g.TranslateTransform(5, 5); + Assert.Multiple(() => + { + Assert.That(g.Transform.OffsetX, Is.EqualTo(15)); + Assert.That(g.Transform.OffsetY, Is.EqualTo(15)); + }); + + g.Restore(state); + Assert.Multiple(() => + { + Assert.That(g.Transform.OffsetX, Is.EqualTo(10)); + Assert.That(g.Transform.OffsetY, Is.EqualTo(10)); + }); + } + + + [Test] + public void Method_BeginAndEndContainer() + { + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.TranslateTransform(10, 10); + + var state = g.BeginContainer(); + Assert.Multiple(() => + { + Assert.That(g.Transform.OffsetX, Is.EqualTo(0)); + Assert.That(g.Transform.OffsetY, Is.EqualTo(0)); + }); + + g.TranslateTransform(5, 5); + Assert.Multiple(() => + { + Assert.That(g.Transform.OffsetX, Is.EqualTo(5)); + Assert.That(g.Transform.OffsetY, Is.EqualTo(5)); + }); + + g.EndContainer(state); + Assert.Multiple(() => + { + Assert.That(g.Transform.OffsetX, Is.EqualTo(10)); + Assert.That(g.Transform.OffsetY, Is.EqualTo(10)); + }); + } + + #endregion + + + #region Draw methods + + [Test] + public void Method_DrawArc() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.DrawArc(pen, 0, 0, 50, 50, 0, 45); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawArc.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawBezier() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.DrawBezier(pen, 0, 25, 5, 15, 15, 5, 25, 0); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawBezier.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawClosedCurve() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var points = new Point[] { new(0, 25), new(25, 0), new(25, 25) }; + var tension = 0.75f; + var mode = FillMode.Winding; + + g.DrawClosedCurve(pen, points, tension, mode); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawClosedCurve.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawCurve() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var points = new Point[] { new(0, 25), new(25, 0), new(25, 25) }; + var tension = 0.75f; + + g.DrawCurve(pen, points, tension); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawCurve.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawEllipse() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.DrawEllipse(pen, 10, 10, 30, 30); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawEllipse.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawIcon() + { + // TODO: test DrawIcon method + } + + [Test] + public void Method_DrawImage() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); + using var image = Image.FromFile(filePath); + + var portion = new Rectangle(10, 10, 30, 30); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.DrawImage(image, 10, 10, portion, GraphicsUnit.Pixel); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawImage.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawLine() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.DrawLine(pen, 10, 10, 40, 40); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawLine.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawPath() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + using var path = new GraphicsPath(); + path.AddLine(0, 0, 50, 0); + path.AddLine(50, 0, 25, 25); + path.AddLine(25, 25, 0, 0); + path.CloseFigure(); // defines a triangle + + g.DrawPath(pen, path); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawPath.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawPie() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.DrawPie(pen, 10, 10, 30, 30, 0, 45); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawPie.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawPolygon() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var points = new[] { new PointF(0, 0), new PointF(25, 25), new PointF(50, 0), new PointF(25, 50) }; + + g.DrawPolygon(pen, points); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawPolygon.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawRectangle() + { + var color = Color.Red; + using var pen = new Pen(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.DrawRectangle(pen, 10, 10, 30, 30); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawRectangle.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_DrawString() + { + string fontPath = Path.Combine(FONT_PATH, "Montserrat-Regular.ttf"); + + using var pfc = new PrivateFontCollection(); + pfc.AddFontFile(fontPath); + + var font = new Font(pfc.Families[0], 10); + + var color = Color.Red; + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var bounds = new RectangleF(0, 0, 40, 40); + + var flags = StringFormatFlags.NoWrap; + var format = new StringFormat(flags) + { + HotkeyPrefix = HotkeyPrefix.Show, + Trimming = StringTrimming.EllipsisCharacter, + }; + + g.TextRenderingHint = TextRenderingHint.SingleBitPerPixel; + g.DrawString("&hello\nworld\n123", font, brush, bounds, format); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"DrawString.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + #endregion + + + #region Fill methods + + [Test] + public void Method_FillClosedCurve() + { + // TODO: test FillClosedCurve method + var color = Color.Red; + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var points = new Point[] { new(0, 25), new(25, 0), new(25, 25) }; + var tension = 0.75f; + var mode = FillMode.Winding; + + g.FillClosedCurve(brush, points, mode, tension); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"FillClosedCurve.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9).Within(0.1)); + }); + } + + [Test] + public void Method_FillEllipse() + { + var color = Color.Red; + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.FillEllipse(brush, 10, 10, 30, 30); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"FillEllipse.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_FillPath() + { + var color = Color.Red; + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + using var path = new GraphicsPath(); + path.AddLine(0, 0, 50, 0); + path.AddLine(50, 0, 25, 25); + path.AddLine(25, 25, 0, 0); + path.CloseFigure(); // defines a triangle + + g.FillPath(brush, path); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"FillPath.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_FillPie() + { + var color = Color.Red; + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.FillPie(brush, 10, 10, 30, 30, 0, 45); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"FillPie.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_FillPolygon() + { + var color = Color.Red; + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var points = new[] { new PointF(0, 0), new PointF(25, 25), new PointF(50, 0), new PointF(25, 50) }; + + g.FillPolygon(brush, points); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"FillPolygon.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_FillRectangle() + { + var color = Color.Red; + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.FillRectangle(brush, 10, 10, 30, 30); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"FillRectangle.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_FillRegion() + { + var color = Color.Red; + using var brush = new SolidBrush(color); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var path = new GraphicsPath(); + path.AddLine(10, 10, 40, 10); + path.AddBezier(40, 10, 30, 40, 20, 40, 10, 10); + path.CloseFigure(); + + using var region = new Region(path); + + g.FillRegion(brush, region); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"FillRegion.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + #endregion + + + #region Text methods + + [Test] + public void Method_MeasureCharacterRanges() + { + string fontPath = Path.Combine(FONT_PATH, "Montserrat-Regular.ttf"); + + using var pfc = new PrivateFontCollection(); + pfc.AddFontFile(fontPath); + + var font = new Font(pfc.Families[0], 10); + + var flags = StringFormatFlags.NoWrap; + var format = new StringFormat(flags) + { + HotkeyPrefix = HotkeyPrefix.Show, + Trimming = StringTrimming.EllipsisCharacter, + }; + + var charRanges = new[] { new CharacterRange(0, 4) }; + format.SetMeasurableCharacterRanges(charRanges); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var bounds = new RectangleF(0, 0, bitmap.Width, bitmap.Height); + + var regions = g.MeasureCharacterRanges("Hello World", font, bounds, format); + Assert.Multiple(() => + { + Assert.That(regions, Has.Length.EqualTo(charRanges.Length)); + var bounds = regions[0].GetBounds(g); + Assert.That(bounds.X, Is.EqualTo(3).Within(1)); + Assert.That(bounds.Y, Is.EqualTo(0).Within(1)); + Assert.That(bounds.Width, Is.EqualTo(27).Within(5)); // NOTE: huge difference but skia draws text's path in pixel, that's why this value is smaller than expected + Assert.That(bounds.Height, Is.EqualTo(17).Within(1)); + }); + } + + [Test] + public void Method_MeasureString() + { + string fontPath = Path.Combine(FONT_PATH, "Montserrat-Regular.ttf"); + + using var pfc = new PrivateFontCollection(); + pfc.AddFontFile(fontPath); + + var font = new Font(pfc.Families[0], 10); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + var measure = g.MeasureString("Hello World", font); + Assert.Multiple(() => + { + Assert.That(measure.Width, Is.EqualTo(85).Within(11)); // NOTE: huge difference but skia draws text's path in pixel, that's why this value is smaller than expected + Assert.That(measure.Height, Is.EqualTo(17).Within(1)); + }); + } + + #endregion + + + #region Clip methods + + [Test] + public void Method_ExcludeClip() + { + var color = Color.Red; + var clip1 = new Rectangle(0, 0, 25, 25); + var clip2 = new Rectangle(10, 10, 30, 30); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.SetClip(clip1); + g.ExcludeClip(clip2); + g.Clear(color); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"ClipExclude.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_IntersectClip() + { + var color = Color.Red; + var clip1 = new Rectangle(0, 0, 25, 25); + var clip2 = new Rectangle(10, 10, 10, 10); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.SetClip(clip1); + g.IntersectClip(clip2); + g.Clear(color); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"ClipIntersect.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_TranslateClip() + { + var color = Color.Red; + var clip = new Rectangle(0, 0, 25, 25); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.SetClip(clip); + g.TranslateClip(10, 10); + g.Clear(color); + Assert.Multiple(() => + { + string filepath = Path.Combine("graphics", $"ClipTranslate.png"); + float similarity = Utils.CompareImage(filepath, bitmap, true); + Assert.That(similarity, Is.GreaterThan(0.9)); + }); + } + + [Test] + public void Method_SetClip() + { + var color = Color.Red; + var clip = new Rectangle(0, 0, 25, 25); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.SetClip(clip); + g.Clear(color); + Assert.Multiple(() => + { + for (int i = 0; i < bitmap.Width; i++) + for (int j = 0; j < bitmap.Height; j++) + Assert.That(bitmap.GetPixel(i, j), Is.EqualTo(clip.Contains(i, j) ? color : Color.Empty)); + }); + } + + [Test] + public void Method_ResetClip() + { + var color = Color.Red; + var clip = new Rectangle(0, 0, 25, 25); + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.SetClip(clip); + g.Clear(color); + g.ResetClip(); + Assert.Multiple(() => + { + for (int i = 0; i < bitmap.Width; i++) + for (int j = 0; j < bitmap.Height; j++) + Assert.That(bitmap.GetPixel(i, j), Is.EqualTo(color)); + }); + } + + #endregion + + + #region Transform methods + + [Test] + public void Method_MultiplyTransform() + { + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + using var matrix = new Matrix(6, 5, 4, 3, 2, 1); + + g.Transform = new Matrix(1, 2, 3, 4, 5, 6); + g.MultiplyTransform(matrix); + Assert.That(g.Transform.Elements, Is.EqualTo(new[] { 21, 32, 13, 20, 10, 14 }).Within(0.001f)); + } + + [Test] + public void Method_ResetTransform() + { + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.TranslateTransform(20, 30); + g.ResetTransform(); + Assert.That(g.Transform.IsIdentity, Is.True); + } + + [Test] + public void Method_RotateTransform() + { + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.RotateTransform(45); + Assert.That(g.Transform.Elements, Is.EqualTo(new[] { 0.707f, 0.707f, -0.707f, 0.707f, 0, 0 }).Within(0.001f)); + } + + [Test] + public void Method_ScaleTransform() + { + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.ScaleTransform(0.50f, 0.25f); + Assert.That(g.Transform.Elements, Is.EqualTo(new[] { 0.5f, 0, 0, 0.25f, 0, 0 }).Within(0.001f)); + } + + [Test] + public void Method_TranslateTransform() + { + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.TranslateTransform(50, 50f); + Assert.That(g.Transform.Elements, Is.EqualTo(new[] { 1, 0, 0, 1, 50, 50 }).Within(0.001f)); + } + + [Test] + [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)] + [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)] + [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 40f, 10f)] + [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)] + [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)] + [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 20f, 20f)] + [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)] + [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 2.5f, 10f)] + [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 5f, 5f)] + [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)] + [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)] + [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 0.4166666f, 0.1041667f)] + [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)] + [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)] + [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 0.2083333f, 0.2083333f)] + [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)] + [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 240f, 960f)] + [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 480f, 480f)] + [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)] + [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)] + [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 10.58333f, 2.645833f)] + [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)] + [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)] + [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 5.291666f, 5.291666f)] + [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)] + [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 9.448818f, 37.79527f)] + [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 18.89764f, 18.89764f)] + [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)] + [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)] + [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 30f, 7.5f)] + [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)] + [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)] + [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 15f, 15f)] + [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)] + [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 3.333333f, 13.33333f)] + [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 6.666666f, 6.666666f)] + [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)] + [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)] + [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 125f, 31.25f)] + [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)] + [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)] + [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 62.5f, 62.5f)] + [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)] + [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 0.8f, 3.2f)] + [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 1.6f, 1.6f)] + public void Method_TransformPoints(GraphicsUnit unit, float scale, CoordinateSpace destination, CoordinateSpace source, float expectedX, float expectedY) + { + var points = new PointF[] { new(10, 10) }; + + using var bitmap = new Bitmap(50, 50); + using var g = Graphics.FromImage(bitmap); + + g.ScaleTransform(0.5f, 2); + + g.PageUnit = unit; + g.PageScale = scale; + + g.TransformPoints(destination, source, points); + Assert.Multiple(() => + { + Assert.That(points[0].X, Is.EqualTo(expectedX).Within(0.001f)); + Assert.That(points[0].Y, Is.EqualTo(expectedY).Within(0.001f)); + }); + } + + #endregion +} \ No newline at end of file diff --git a/test/Common/res/images/graphics/ClipExclude.png b/test/Common/res/images/graphics/ClipExclude.png new file mode 100644 index 0000000000000000000000000000000000000000..77c0ba90df25cafe3fb391bb89de613b33d8d327 GIT binary patch literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;Au}6$B+!?x4nj3%nm$<-~FGT$5|oN;`1s% z=Yk0P-t*P+x4&~snEIS*4LrOgh7jZ8*)#q>%%43u^djw<`XA}yqQny8&%*zDEq5C1 zn)#FS|FJnS5_ix1X{k^4)9cAgt2?ycP+i>3+}Qr%{TY+F7tfwK|3m(nldYJ>TrtoT tcjKL0l!(Mw8RVt8bP9wxRsZRpxM`Z0a@<|!lvI6;>1s;*b3=DjSL74G){)!Z!V1=iPV@QVc+jE9|4Gs)U2Y>%xel!lvI6;>1s;*b3=DjSL74G){)!Z!;8af+$B+!?w`Ug$9xz}yeBjOg5@`h$J(1Tr zf6~`(y%wea?e5j~EaNDdRcA8_SC}n6y6{$y7;OZivOyMR@4aAY@1}A$`peuseLx2? Nc)I$ztaD0e0sw;=W4-_Y literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/DrawArc.png b/test/Common/res/images/graphics/DrawArc.png new file mode 100644 index 0000000000000000000000000000000000000000..38899912d1f3ddf7e83738ff966874df817fa00e GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!poOQ4V@QVc+lvP|84MVh48Oy85}Sb4q9e0Cfm1 A@&Et; literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/DrawBezier.png b/test/Common/res/images/graphics/DrawBezier.png new file mode 100644 index 0000000000000000000000000000000000000000..2f12657f03ef887070a791d045689fa0fed95970 GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V34PaV@QVc+l!7|2OKz99RL4+$=mkVQK2WO zm3{r$EAr>6n@bP8==jK(>)IeH)Zt-hw}?5+H%|Zn literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/DrawClosedCurve.png b/test/Common/res/images/graphics/DrawClosedCurve.png new file mode 100644 index 0000000000000000000000000000000000000000..bd23bceb89203ae770cf7a7e68396a3c86766b7b GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;8sr;$B+!?xBbyvM+|tFi~oC$M1eLUiI_(oGM8Xn_#1@C$1K0roNlvrCzi>Kx?9UYEZ|5 zBRxvqscCKc!ZT&}xU>jri6^p76VOy-T%vTwU?zvPhteXBh3Oh94xAH(gc@cop2VQ) zqEpD$9pf<3k@w(zp-w(!A1wnTWr_MPovTfITz?&T)&6(3-Nth#_vxsG9g|4zF$58b b7jLmEKVKZ{UsT=(^Z!lvI6;>1s;*b3=DjSL74G){)!Z!;6zUs$B+!?x92VS8Vq0(CKI;ttDAyy||EfAX^ z#-SI@@azopr0M-0k^8f$< literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/DrawEllipse.png b/test/Common/res/images/graphics/DrawEllipse.png new file mode 100644 index 0000000000000000000000000000000000000000..3e2711935d4a9122ef162a10b80e07e1b13ae59d GIT binary patch literal 269 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;37{K$B+!?x96j|S_}kSF21S1Yml*>?L;2}%yMN33?_2%-#=Y{gnTw6?Pg`Y`l;!jF#}tXHD@@&N(>gcy zlbE}RB35FzJ z>j<#5QdJ7+_3!2QEx5QnW#81!#rODL*M8@Gp1l75V@?K!6<2R@t;|^-IY-dtCeXDE Mp00i_>zopr0P(wMF8}}l literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/DrawImage.png b/test/Common/res/images/graphics/DrawImage.png new file mode 100644 index 0000000000000000000000000000000000000000..2ea432730701202a42cf3ea6833ca7baaf7be784 GIT binary patch literal 1025 zcmV+c1pfPpP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1C~idK~!i%?Uu_= z6Hye#@t=@~2{FjZ#D#8Lkr=ne#Eo(1#tn&yd;JI4xzNO&iMl}(G`eyj@)odEL#YIU zKpz%LTM){#O>2(xyWANklPglWX-Jqa`I0-+nSS4ybIv`tAw-A}Awq-*aT2k$wbi(@ zvlDYDrBX?%)#`z)R4T%=|Kh3ZT?bOj+k+$e*B~$r-Rp9>Y&316$-JLh-X0M32Z334 zy|-IbbniRaQw*9m(PZ9FEpHEqy2fF#FypQFwpSDhBAfamQ{&H#rcE@N_fyN;1KDuP zN7eu8sF#Jk;+(7{dz7(EIw-#y9a^G34=QOnx{8K@Lq$ghPP>Z@azY^;wpDdUhe_Xu#Yze6@6=VWu@tk*OD zLoII){3%;2O-%{$^)r2E3-x&AnOzF%3aKwY)tb z%A8=}Q8tlR?Ou;CJ0NRFX;FSWOF8@1QQeT=zrlBb*f*})bAgbA& zjere-GUNVIWw=fK#xtq;nA))tGw&rn(q%lV zn9TdBkgc|WzhJ&+L&fbQ9el400!t#&4d%nXL$LhPA(=2#vY zsc*j_zBuMttF74~YXT%Q?sL3ztd8+PfvM3aUfD1sp!*w*?XHhXEpHDd5tj@)=w2VC zl!Daq_FximiE*3e{nYaIU=pbZwY)u;MCw5;Zx1GsdQi*TgK1dRgIeAmOd|E5mbV9! vNIj_K?ZG5c4{G@kAwq-*5hBD1LPNuEW4Th^cQ$-$00000NkvXXu0mjfD>2!0 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/DrawLine.png b/test/Common/res/images/graphics/DrawLine.png new file mode 100644 index 0000000000000000000000000000000000000000..4031ace9ccb8bf2c3f3ae15fcd2becb24c76d70d GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!ptGloV@QVc+w+W^3?GIOO-aO$#CK2v9Ep$PCWyfG!lvI6;>1s;*b3=DjSL74G){)!Z!;CxRP$B+!?x96j|8Vqa&aI-Waa`mfHJsi*~fn<#{@Wde&~o zcifW~z5Fz<#OHc-prK-Ul{{zXgt+5Q%13^`n85MT%Fe_o%6&=4g!Rcmt|x8>YAQc@ zx#Nmpk@+Tx89r66t0ua~J)z4*}Q$iB} Dq6c6$ literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/DrawPie.png b/test/Common/res/images/graphics/DrawPie.png new file mode 100644 index 0000000000000000000000000000000000000000..7b3f0571f0131ef241fc3d72d4376f1b953839bb GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V6vx+V@QVc+w+cG4F)`}2k-sAly#tq`)HJ) z(9DH*+x|-d)i8W0KK!lvI6;>1s;*b3=DjSL74G){)!Z!1_@6W$B+!?yZwQDhYdKKi~oD)e_9e2d)vgp zhs)+|vKQm;t^SACGYFj2+x-2!&gbj3zc<^fJlyvEZF0h{+J64&{Rbt=WcgSeu6+En z(E5kas=8GVTUS)y<>i?^$FST;-^$?XQgaPEho(!-MJY^C|1LNEnjutW_IClp^fMP1 z{VZU=Zgc%H`<-GH_B7|Pls_MMKJBS>tn!e4QTko5OrzSb=h=gdX_cp07Bf6PW^(KB z^Pe|swXPXmvo+UYTwQjhqFrYqL#j(d_>CaW05_Hm&ha2FgZ7_L5ckY94n@XO9Sqqs z1B4dza9o(ir3vC@?z{lvN=)?u$u`8@3;}U1ru%_p8`jojac*#1vQ!lvI6;>1s;*b3=DjSL74G){)!Z!V2r1WV@QVc+w<032Ml;z9Pj_X^t0i%X`|_m z6FfoMU)UHvxL-G3Ubb(;@o&xYoSrRHT2!38oP-or6+0(Pn&9H$>7lelMMW@3XcC7e lMEd=~VC#4WhHJ-mFtT2GFDPZ&HxXzLgQu&X%Q~loCIC2iJ_`T< literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/DrawString.png b/test/Common/res/images/graphics/DrawString.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9d26c619491a82e96ecb3138281d7f0add1573 GIT binary patch literal 403 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!1`SUa$B+!?w^J|n9X8-#`TXDhZWqTQ-Bj5r zH&$xBeQ{xCWY(JM9eGYR2eSX~^xv0$ZEtRx?wRjrc5OeqruFB%$nV=aTfa$X^9wJY z>DT|XO77~>Ge18v#GZ4X8Mrfg1NY|j&n`TznZkM|e$nnDzgF4kJ^%Y-Dwx-DmUZ#Z zIbi0JS%2Il(`u`5?nTcX_7^wL6eStjEhut($NVKH&2pQC ze*e76CB~m6t`>b$_#Cm0|D239(9|FLZ|Wzz9EZhvE2*BP}%ZSNu}U_df>y85}Sb4q9e0K*oxvj6}9 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/FillClosedCurve.png b/test/Common/res/images/graphics/FillClosedCurve.png new file mode 100644 index 0000000000000000000000000000000000000000..53182268e6f214cc6ec246c07160a7eb4c125e35 GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;7Lyx$B+!?w|$O$hYSRc{QB?D%q~BHb;>j! zsU;gfbf(!x-+cJ($KFZ%|1xi;&UffzW%UabJ05~n`h#dmh${RL(iQ%s_b^+Wo5+y4NH6W!lvI6;>1s;*b3=DjSL74G){)!Z!;2}>J$B+!?w|$0uha3cseERRt+$tTT(3ECA zQQ~#Xy{f(-FU_S>>>qrrx_@eV^}~tjPmX-8xS?P4O3Fq}aryTT4NFwY3T!kxD`n4hoImm+0uPACy`H&dB=bRw_k^MVBLPpCo&RdVfWs+P^k zlPAqNe2~-g?BjwUA#00yQ;x{VO_*@``UAuLCsw}xxWTySQC!6n^`BsOeL7ZX!}#Pv W-i;fa$7+DSVeoYIb6Mw<&;$U~FLhl2 literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/FillPath.png b/test/Common/res/images/graphics/FillPath.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa17d07599113bd00d1156ec20d55e2df6246c0 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;7(5$$B+!?x92_i4jb?|2j2aENi*s~cg>be zHvUZq*$XD$5R#2Pvd!sHuYCIH=Yp*D=Y+1_f4uNb;M?~V7TF=+CG=Cfv*sN>9l5CL zdclboE3Vnfm}#y0%+vm@FYC&|hkF-&EiLqXoslaYb4@Giy4dtGme7bef$J&iY zE@!-v&?z>TG8glX?LGG7KvDYQ*E=^n+5aNSPv^I`)jVl$V=FWEY5Eu6@6`F&k<{rv gVpF literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/FillPie.png b/test/Common/res/images/graphics/FillPie.png new file mode 100644 index 0000000000000000000000000000000000000000..3b356eee81ec5122cfdf42ba9a2bfafbeac6a9c5 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!V5X;wV@QVc+w*~3Ee1R+7w-MP^qNcABOz9> zWy_xS&sSa4n1QMoBJMo@ZR=Zi(OtK~Jo5e0X?rHG`4-&$&-+@{s?_z9rtZsn7=N-N ze`|^Fv1b9N=TDwkd4>7glT15rnZk%?0t^fl7bT?iwjZ}#apa;L$T&||KbLh*2~7Y) C5=Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0dh%1K~!i%?T-O& zg&+_Gz5k`#*bF~p6hs7spH4bSTi)(Iw|^$$=kF)}WkC+zSNxd0^XZ2i{y8z(zS+l# z2tAWsF9Q=$+_556ZIn@^*Z7pZU;Nvaj#29?Mm8>6&OSF*HV{wV6 zvi2QZ=83F)2bZapb?@R*HL~hmT!lvI6;>1s;*b3=DjSL74G){)!Z!;6_gu$B+!?w`Ug$GBXMsKJb43OBDx?h{p}~ zy}jReu6w;}-Rs`ky5Gki?&ev;cRw|u^oJhv>ci!d5wiPEA9(d4mMyIPz0n4%n(2*K lX{zLUY3%jVy(^yoWU{=$m}G1xe;Vj`22WQ%mvv4FO#r4!b!lvI6;>1s;*b3=DjSL74G){)!Z!;CfFN$B+!?xBcFHhXQzx-21D4*8LMxuNF(L zT4l$^536!*)(ALpDE?92emvjl)7Fe7&X>T%*NhXIp8Qh9UYDAW&6)INp=0}!JsDRN zHT9)RJx=wvX{%mrJ_aP(`BqH0y!S;1m*w4lzMV?y=@$&cpFEMVdeJjymaF^RiNT*= zY{-9d#O}ob|0kFKURe14$>iFL2i-rt`uE~Pc;(ab3-%wTxGT1RoX{fhEorUw% T(P`2kKQnl``njxgN@xNA#5{7h literal 0 HcmV?d00001 diff --git a/test/Common/res/images/graphics/RenderingOrigin_00-00.png b/test/Common/res/images/graphics/RenderingOrigin_00-00.png new file mode 100644 index 0000000000000000000000000000000000000000..3e0257107c74ca076b1201bc514a04cba558daab GIT binary patch literal 507 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!h6$c7jv*P&ch4QnJ?y~48fZGPE7| z4S1NiKS@qg{dGWwZMTHnznt6K-v4Jl)fwQ|QlvCRc;Ot5r!E@mPL_f|p|E}Xw*P(q z-0{!%`|PV!lvI6;>1s;*b3=DjSL74G){)!Z!h8|BB$B+!?yXOw(K5^h-4OD&DVa_vO_IR6G zw1Jeu33Z2A3;Dh*ywLlL{o1zN-~Y@P%;9+IqM`0&DR`^c~Q_94Fd4ADE Rb6{jKc)I$ztaD0e0svV)<39iZ literal 0 HcmV?d00001 From 6c898119529e98ab3f122130fef37418cabfac1b Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 15:07:34 -0300 Subject: [PATCH 213/217] add SolidBrush to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8c83bcb..c9f43b6 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ Basic graphics funcionalities based on `System.Drawing`. | `Image` | Class | Represents an image in a specific format. | `Pen` | Class | Defines an object used to draw lines and curves. | `Region` | Class | Defines the area of a drawing surface. +| `SolidBrush` | Class | Defines a brush of a single colo. | `Svg` (1) | Class | Represents Scalable Vector Graphics. | `TextureBrush` | Class | Defines a brush that uses an image to fill shapes. | `Point` | Struct | Defines an x and y coordinate in a 2D plane. From e680b2cd2c40ce7da02981cc09b21f0fdd355440 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 15:11:07 -0300 Subject: [PATCH 214/217] add StringFormat to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c9f43b6..7d43fe2 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ Basic graphics funcionalities based on `System.Drawing`. | `Image` | Class | Represents an image in a specific format. | `Pen` | Class | Defines an object used to draw lines and curves. | `Region` | Class | Defines the area of a drawing surface. -| `SolidBrush` | Class | Defines a brush of a single colo. +| `SolidBrush` | Class | Defines a brush of a single color. +| `StringFormat` | Class | Defines text layout information, display manipulation and font features. | `Svg` (1) | Class | Represents Scalable Vector Graphics. | `TextureBrush` | Class | Defines a brush that uses an image to fill shapes. | `Point` | Struct | Defines an x and y coordinate in a 2D plane. From 941db7be728466316f178c7b91d5bb9741026b24 Mon Sep 17 00:00:00 2001 From: damiansalvia Date: Mon, 16 Sep 2024 15:48:56 -0300 Subject: [PATCH 215/217] adjust thresholds for DrawString, MeasureString and MeasureCharacterRanges of Graphics class --- test/Common/GraphicsUnitTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Common/GraphicsUnitTest.cs b/test/Common/GraphicsUnitTest.cs index 57aefc9..6b6cf77 100644 --- a/test/Common/GraphicsUnitTest.cs +++ b/test/Common/GraphicsUnitTest.cs @@ -475,7 +475,7 @@ public void Method_DrawString() { string filepath = Path.Combine("graphics", $"DrawString.png"); float similarity = Utils.CompareImage(filepath, bitmap, true); - Assert.That(similarity, Is.GreaterThan(0.9)); + Assert.That(similarity, Is.GreaterThan(0.85)); }); } @@ -668,7 +668,7 @@ public void Method_MeasureCharacterRanges() Assert.That(bounds.X, Is.EqualTo(3).Within(1)); Assert.That(bounds.Y, Is.EqualTo(0).Within(1)); Assert.That(bounds.Width, Is.EqualTo(27).Within(5)); // NOTE: huge difference but skia draws text's path in pixel, that's why this value is smaller than expected - Assert.That(bounds.Height, Is.EqualTo(17).Within(1)); + Assert.That(bounds.Height, Is.EqualTo(17).Within(2)); }); } @@ -689,7 +689,7 @@ public void Method_MeasureString() Assert.Multiple(() => { Assert.That(measure.Width, Is.EqualTo(85).Within(11)); // NOTE: huge difference but skia draws text's path in pixel, that's why this value is smaller than expected - Assert.That(measure.Height, Is.EqualTo(17).Within(1)); + Assert.That(measure.Height, Is.EqualTo(17).Within(2)); }); } From a94ebf418fa68f5081dceabeb5c08782480bc2cd Mon Sep 17 00:00:00 2001 From: Damian Salvia Varela Date: Fri, 20 Sep 2024 13:30:29 -0300 Subject: [PATCH 216/217] remove ceiling defining the underline when adding string to GraphicsPath --- src/Common/Drawing2D/GraphicsPath.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs index 67ce580..529eb1f 100644 --- a/src/Common/Drawing2D/GraphicsPath.cs +++ b/src/Common/Drawing2D/GraphicsPath.cs @@ -982,7 +982,6 @@ private void AddString(string text, FontFamily family, int style, float emSize, underlineTop, origin + length + rtlOffsetX, underlineTop + underlineHeight); - underline = SKRectI.Ceiling(underline); textPath.AddRect(underline); } From 0b971181374520d0b4cc49138964e27f8b1c7039 Mon Sep 17 00:00:00 2001 From: Claudia Murialdo Date: Wed, 8 Jan 2025 19:19:50 -0300 Subject: [PATCH 217/217] Change properties A, R, G, and B from int to the expected byte type for consistency --- src/Common/Bitmap.cs | 2 +- src/Common/Color.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 188e765..fe99353 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -264,7 +264,7 @@ protected override void RotateFlip(int degrees, float scaleX, float scaleY) /// public void SetPixel(int x, int y, Color color) { - var c = new SKColor((byte)color.R, (byte)color.G, (byte)color.B, (byte)color.A); + var c = new SKColor(color.R, color.G, color.B, color.A); m_bitmap.SetPixel(x, y, c); } diff --git a/src/Common/Color.cs b/src/Common/Color.cs index 87082f0..08ce15f 100644 --- a/src/Common/Color.cs +++ b/src/Common/Color.cs @@ -143,22 +143,22 @@ public readonly bool IsSystemColor /// /// Gets the alpha component value of this structure. /// - public readonly int A => m_color.Alpha; + public readonly byte A => m_color.Alpha; /// /// Gets the red component value of this structure. /// - public readonly int R => m_color.Red; + public readonly byte R => m_color.Red; /// /// Gets the green component value of this structure. /// - public readonly int G => m_color.Green; + public readonly byte G => m_color.Green; /// /// Gets the blue component value of this structure. /// - public readonly int B => m_color.Blue; + public readonly byte B => m_color.Blue; /// /// Gets the name component value of this structure.