diff --git a/CHANGELOG.md b/CHANGELOG.md index de78dec..c7aa94d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 3.2.0 ### Added - X10D: Added `MathUtility.InverseLerp(float, float, float)` and `MathUtility.InverseLerp(double, double, double)` -- X10D: Added `Circle`, `CircleF`, `Ellipse`, `EllipseF` `Line`, `LineF`, `Polygon`, and `PolygonF`, to complement System.Drawing structs such as `Point` and `Rectangle` +- X10D: Added `Circle`, `CircleF`, `Ellipse`, `EllipseF` `Line`, `LineF`, `Line3D`, `Polygon`, and `PolygonF`, to complement System.Drawing structs such as `Point` and `Rectangle` - X10D: Added `Point.ToSize()` - X10D: Added `Point.ToSizeF()` - X10D: Added `Point.ToVector2()` diff --git a/X10D.Tests/src/Drawing/Line3DTests.cs b/X10D.Tests/src/Drawing/Line3DTests.cs new file mode 100644 index 0000000..32335d9 --- /dev/null +++ b/X10D.Tests/src/Drawing/Line3DTests.cs @@ -0,0 +1,182 @@ +using System.Drawing; +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using X10D.Drawing; + +namespace X10D.Tests.Drawing; + +[TestClass] +public class Line3DTests +{ + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyAndOne() + { + Assert.AreEqual(-1, Line3D.Empty.CompareTo(Line3D.One)); + } + + [TestMethod] + public void CompareTo_ShouldBeNegativeOne_GivenEmptyLineAndOneLineAsObject() + { + Assert.AreEqual(-1, Line3D.Empty.CompareTo((object)Line3D.One)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenNull() + { + Assert.AreEqual(1, Line3D.One.CompareTo(null)); + } + + [TestMethod] + public void CompareTo_ShouldBeOne_GivenOneAndEmpty() + { + Assert.AreEqual(1, Line3D.One.CompareTo(Line3D.Empty)); + } + + [TestMethod] + public void CompareTo_ShouldBeZero_GivenUnitLine() + { + var unitLine3D = Line3D.One; + Assert.AreEqual(0, unitLine3D.CompareTo(unitLine3D)); + } + + [TestMethod] + public void CompareTo_ShouldThrowArgumentException_GivenInvalidType() + { + Assert.ThrowsException(() => Line3D.Empty.CompareTo(new object())); + } + + [TestMethod] + public void Length_ShouldBe0_GivenEmptyLine() + { + Assert.AreEqual(0.0f, Line3D.Empty.Length); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitXLine() + { + Assert.AreEqual(1.0f, Line3D.UnitX.Length, 1e-6f); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitYLine() + { + Assert.AreEqual(1.0f, Line3D.UnitY.Length, 1e-6f); + } + + [TestMethod] + public void Length_ShouldBe1_GivenUnitZLine() + { + Assert.AreEqual(1.0f, Line3D.UnitZ.Length, 1e-6f); + } + + [TestMethod] + public void Equals_ShouldBeTrue_GivenTwoUnitLines() + { + Line3D first = Line3D.One; + Line3D second = Line3D.One; + + Assert.AreEqual(first, second); + Assert.IsTrue(first == second); + Assert.IsFalse(first != second); + } + + [TestMethod] + public void Equals_ShouldBeFalse_GivenDifferentLines() + { + Assert.AreNotEqual(Line3D.One, Line3D.Empty); + Assert.IsFalse(Line3D.One == Line3D.Empty); + Assert.IsTrue(Line3D.One != Line3D.Empty); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenEmptyLine() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Line3D.Empty.GetHashCode(); + Assert.AreEqual(hashCode, Line3D.Empty.GetHashCode()); + } + + [TestMethod] + public void GetHashCode_ShouldBeCorrect_GivenUnitLine() + { + // this test is pretty pointless, it exists only for code coverage purposes + int hashCode = Line3D.One.GetHashCode(); + Assert.AreEqual(hashCode, Line3D.One.GetHashCode()); + } + + [TestMethod] + public void op_Explicit_ShouldReturnEquivalentLine_GivenLine() + { + Line3D oneLine = new Line3D(Vector3.Zero, Vector3.UnitX + Vector3.UnitY); + Line converted = (Line)oneLine; + + var expectedStart = new Point((int)oneLine.Start.X, (int)oneLine.Start.Y); + var expectedEnd = new Point((int)oneLine.End.X, (int)oneLine.End.Y); + + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(expectedStart, converted.Start); + Assert.AreEqual(expectedEnd, converted.End); + } + + [TestMethod] + public void op_Explicit_ShouldReturnEquivalentLineF_GivenLine() + { + Line3D oneLine = new Line3D(Vector3.Zero, Vector3.UnitX + Vector3.UnitY); + LineF converted = (LineF)oneLine; + + var expectedStart = new PointF(oneLine.Start.X, oneLine.Start.Y); + var expectedEnd = new PointF(oneLine.End.X, oneLine.End.Y); + + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(expectedStart, converted.Start); + Assert.AreEqual(expectedEnd, converted.End); + } + + [TestMethod] + public void op_GreaterThan_True_GivenUnitAndEmptyCircle() + { + Assert.IsTrue(Line3D.One > Line3D.Empty); + Assert.IsTrue(Line3D.One >= Line3D.Empty); + Assert.IsFalse(Line3D.One < Line3D.Empty); + Assert.IsFalse(Line3D.One <= Line3D.Empty); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentLine_GivenLine() + { + Line oneLine = Line.One; + Line3D converted = oneLine; + + var expectedStart = new Vector3(oneLine.Start.X, oneLine.Start.Y, 0.0f); + var expectedEnd = new Vector3(oneLine.End.X, oneLine.End.Y, 0.0f); + + Assert.AreEqual(oneLine, converted); + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(expectedStart, converted.Start); + Assert.AreEqual(expectedEnd, converted.End); + } + + [TestMethod] + public void op_Implicit_ShouldReturnEquivalentLine_GivenLineF() + { + LineF oneLine = LineF.One; + Line3D converted = oneLine; + + var expectedStart = new Vector3(oneLine.Start.X, oneLine.Start.Y, 0.0f); + var expectedEnd = new Vector3(oneLine.End.X, oneLine.End.Y, 0.0f); + + Assert.AreEqual(oneLine, converted); + Assert.AreEqual(oneLine.Length, converted.Length); + Assert.AreEqual(expectedStart, converted.Start); + Assert.AreEqual(expectedEnd, converted.End); + } + + [TestMethod] + public void op_LessThan_True_GivenEmptyAndUnitCircle() + { + Assert.IsTrue(Line3D.Empty < Line3D.One); + Assert.IsTrue(Line3D.Empty <= Line3D.One); + Assert.IsFalse(Line3D.Empty > Line3D.One); + Assert.IsFalse(Line3D.Empty >= Line3D.One); + } +} diff --git a/X10D.Tests/src/Drawing/LineFTests.cs b/X10D.Tests/src/Drawing/LineFTests.cs index 23548be..24b5d10 100644 --- a/X10D.Tests/src/Drawing/LineFTests.cs +++ b/X10D.Tests/src/Drawing/LineFTests.cs @@ -64,11 +64,12 @@ public class LineFTests [TestMethod] public void Equals_ShouldBeTrue_GivenTwoUnitLines() { - var unitLineF1 = LineF.One; - var unitLineF2 = LineF.One; - Assert.AreEqual(unitLineF1, unitLineF2); - Assert.IsTrue(unitLineF1 == unitLineF2); - Assert.IsFalse(unitLineF1 != unitLineF2); + LineF first = LineF.One; + LineF second = LineF.One; + + Assert.AreEqual(first, second); + Assert.IsTrue(first == second); + Assert.IsFalse(first != second); } [TestMethod] diff --git a/X10D.Tests/src/Drawing/LineTests.cs b/X10D.Tests/src/Drawing/LineTests.cs index db710e5..6fce015 100644 --- a/X10D.Tests/src/Drawing/LineTests.cs +++ b/X10D.Tests/src/Drawing/LineTests.cs @@ -46,11 +46,12 @@ public class LineTests [TestMethod] public void Equals_ShouldBeTrue_GivenTwoUnitLines() { - var unitLine1 = Line.One; - var unitLine2 = Line.One; - Assert.AreEqual(unitLine1, unitLine2); - Assert.IsTrue(unitLine1 == unitLine2); - Assert.IsFalse(unitLine1 != unitLine2); + Line first = Line.One; + Line second = Line.One; + + Assert.AreEqual(first, second); + Assert.IsTrue(first == second); + Assert.IsFalse(first != second); } [TestMethod] diff --git a/X10D/src/Drawing/Line.cs b/X10D/src/Drawing/Line.cs index 8b81fb9..8fbd726 100644 --- a/X10D/src/Drawing/Line.cs +++ b/X10D/src/Drawing/Line.cs @@ -3,7 +3,7 @@ namespace X10D.Drawing; /// -/// Represents a line that is composed of 32-bit signed integer X and Y coordinates. +/// Represents a line in 2D space that is composed of 32-bit signed integer X and Y coordinates. /// public readonly struct Line : IEquatable, IComparable, IComparable { diff --git a/X10D/src/Drawing/Line3D.cs b/X10D/src/Drawing/Line3D.cs new file mode 100644 index 0000000..c375ed2 --- /dev/null +++ b/X10D/src/Drawing/Line3D.cs @@ -0,0 +1,330 @@ +using System.Drawing; +using System.Numerics; + +namespace X10D.Drawing; + +/// +/// Represents a line in 3D space that is composed of 32-bit signed integer X, Y and Z coordinates. +/// +public readonly struct Line3D : IEquatable, IComparable, IComparable +{ + /// + /// The empty line. That is, a line whose start and end points are at (0, 0). + /// + public static readonly Line3D Empty = new(); + + /// + /// The line whose start point is at (0, 0, 0) and end point is at (1, 1, 1). + /// + public static readonly Line3D One = new(Vector3.Zero, Vector3.One); + + /// + /// The line whose start point is at (0, 0, 0) and end point is at (1, 0, 0). + /// + public static readonly Line3D UnitX = new(Vector3.Zero, Vector3.UnitX); + + /// + /// The line whose start point is at (0, 0, 0) and end point is at (0, 1, 0). + /// + public static readonly Line3D UnitY = new(Vector3.Zero, Vector3.UnitY); + + /// + /// The line whose start point is at (0, 0, 0) and end point is at (0, 0, 1). + /// + public static readonly Line3D UnitZ = new(Vector3.Zero, Vector3.UnitZ); + + /// + /// Initializes a new instance of the struct by taking the start and end points. + /// + /// The start point. + /// The end point. + public Line3D(Vector3 start, Vector3 end) + { + End = end; + Start = start; + } + + /// + /// Gets the end point of the line. + /// + /// The end point. + public Vector3 End { get; } + + /// + /// Gets the length of this line. + /// + /// The length. + public float Length + { + get => (End - Start).Length(); + } + + /// + /// Gets the length of this line, squared. + /// + /// The squared length. + public float LengthSquared + { + get => (End - Start).LengthSquared(); + } + + /// + /// Gets the start point of the line. + /// + /// The start point. + public Vector3 Start { get; } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator ==(Line3D left, Line3D right) + { + return left.Equals(right); + } + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// if and are considered not equal; otherwise, + /// . + /// + public static bool operator !=(Line3D left, Line3D right) + { + return !left.Equals(right); + } + + /// + /// Returns a value indicating whether the length of one line is less than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than that of + /// ; otherwise, . + /// + public static bool operator <(Line3D left, Line3D right) + { + return left.CompareTo(right) < 0; + } + + /// + /// Returns a value indicating whether the length of one line is greater than that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than that of + /// ; otherwise, . + /// + public static bool operator >(Line3D left, Line3D right) + { + return left.CompareTo(right) > 0; + } + + /// + /// Returns a value indicating whether the length of one line is less than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is less than or equal to that of + /// ; otherwise, . + /// + public static bool operator <=(Line3D left, Line3D right) + { + return left.CompareTo(right) <= 0; + } + + /// + /// Returns a value indicating whether the length of one line is greater than or equal to that of another. + /// + /// The first instance. + /// The second instance. + /// + /// if the of is greater than or equal to that of + /// ; otherwise, . + /// + public static bool operator >=(Line3D left, Line3D right) + { + return left.CompareTo(right) >= 0; + } + + /// + /// Explicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static explicit operator Line(Line3D line) + { + Vector3 start = line.Start; + Vector3 end = line.End; + return new Line(new Point((int)start.X, (int)start.Y), new Point((int)end.X, (int)end.Y)); + } + + /// + /// Explicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static explicit operator LineF(Line3D line) + { + Vector3 start = line.Start; + Vector3 end = line.End; + return new LineF(new PointF(start.X, start.Y), new PointF(end.X, end.Y)); + } + + /// + /// Implicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static implicit operator Line3D(Line line) + { + Point start = line.Start; + Point end = line.End; + return new Line3D(new Vector3(start.X, start.Y, 0), new Vector3(end.X, end.Y, 0)); + } + + /// + /// Implicitly converts a to a . + /// + /// The line to convert. + /// The converted line. + public static implicit operator Line3D(LineF line) + { + PointF start = line.Start; + PointF end = line.End; + return new Line3D(new Vector3(start.X, start.Y, 0), new Vector3(end.X, end.Y, 0)); + } + + /// + /// Compares this instance to another object. + /// + /// The object with with which to compare + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// + /// Comparison internally measures the property to avoid calls to . + /// is not an instance of . + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return 1; + } + + if (obj is not Line3D other) + { + throw new ArgumentException($"Object must be of type {GetType()}"); + } + + return CompareTo(other); + } + + /// + /// Compares this instance to another . + /// + /// + /// + /// A signed number indicating the relative values of this instance and . + /// + /// + /// + /// Return value + /// Meaning + /// + /// + /// + /// Less than zero + /// + /// The of this instance is less than that of . + /// + /// + /// + /// Zero + /// + /// This instance is equal to , or the of both this instance + /// and are not a number (), + /// , or . + /// + /// + /// + /// Greater than zero + /// + /// The of this instance is greater than that of . + /// + /// + /// + /// + /// + /// Comparison internally measures the property to avoid calls to . + /// + public int CompareTo(Line3D other) + { + return LengthSquared.CompareTo(other.LengthSquared); + } + + /// + public override bool Equals(object? obj) + { + return obj is Line3D other && Equals(other); + } + + /// + /// Returns a value indicating whether this instance and another instance are equal. + /// + /// The instance with which to compare. + /// + /// if this instance and are considered equal; otherwise, + /// . + /// + public bool Equals(Line3D other) + { + return End.Equals(other.End) && Start.Equals(other.Start); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(End, Start); + } +} diff --git a/X10D/src/Drawing/LineF.cs b/X10D/src/Drawing/LineF.cs index 2b50963..a715a5c 100644 --- a/X10D/src/Drawing/LineF.cs +++ b/X10D/src/Drawing/LineF.cs @@ -5,7 +5,7 @@ using X10D.Numerics; namespace X10D.Drawing; /// -/// Represents a line that is composed of single-precision floating-point X and Y coordinates. +/// Represents a line in 2D space that is composed of single-precision floating-point X and Y coordinates. /// public readonly struct LineF : IEquatable, IComparable, IComparable {