using Azimuth; using System; using UnitTestSharp; namespace Annulus.UnitTests { public class OrientedBoxTests : TestFixture { private static Rect RectAt(Scalar x, Scalar y, Scalar width, Scalar height) { return Rect.FromPositionAndSize(new Vector(x, y), new Vector(width, height)); } private static Vector[] Corners(OrientedBox rect) { return new[] { rect.Origin, rect.Origin + rect.SideOffset, rect.Origin + rect.CrossOffset, rect.Origin + rect.SideOffset + rect.CrossOffset, }; } private static AffineMatrix Turn(double radians) { return new AffineMatrix(radians, Vector.Zero); } public class CtorTests : TestFixture { public void Always_KeepsTheThreeParts() { var rect = new OrientedBox(new Vector(1, 2), new Vector(3, 4), -5); CheckEqual(new Vector(1, 2), rect.Origin); CheckEqual(new Vector(3, 4), rect.SideOffset); CheckEqual(-5, rect.CrossLength); } } public class FromRectTests : TestFixture { public void Always_StartsAtTheTopLeftAndRunsRightThenDown() { OrientedBox rect = OrientedBox.FromRect(RectAt(1, 2, 30, 40)); CheckEqual(new Vector(1, 2), rect.Origin); CheckEqual(new Vector(30, 0), rect.SideOffset); CheckEqual(40, rect.CrossLength); } public void NegativeSize_StartsAtTheTopLeftOfTheSameArea() { OrientedBox rect = OrientedBox.FromRect( Rect.FromPositionAndSize(new Vector(31, 42), new Vector(-30, -40))); CheckEqual(OrientedBox.FromRect(RectAt(1, 2, 30, 40)), rect); } public void ImplicitConversion_IsTheSameAsFromRect() { OrientedBox converted = RectAt(1, 2, 30, 40); CheckEqual(OrientedBox.FromRect(RectAt(1, 2, 30, 40)), converted); } public void ImplicitConversion_LiftsToNullables() { Rect? none = null; OrientedBox? converted = none; CheckFalse(converted.HasValue); } } public class IsEmptyTests : TestFixture { public void NoSideOffset_IsEmpty() { CheckTrue(new OrientedBox(Vector.Zero, Vector.Zero, 5).IsEmpty); } public void NoCrossLength_IsEmpty() { CheckTrue(new OrientedBox(Vector.Zero, new Vector(5, 0), 0).IsEmpty); } public void Otherwise_IsNotEmpty() { CheckFalse(new OrientedBox(Vector.Zero, new Vector(5, 0), -1).IsEmpty); } } public class CrossOffsetTests : TestFixture { public void PositiveCrossLength_RunsDownFromAnUprightBox() { CheckEqual(new Vector(0, 5), new OrientedBox(Vector.Zero, new Vector(10, 0), 5).CrossOffset); } public void NegativeCrossLength_RunsUpFromAnUprightBox() { CheckEqual(new Vector(0, -5), new OrientedBox(Vector.Zero, new Vector(10, 0), -5).CrossOffset); } public void Always_IsAQuarterTurnFromSideOffsetAtTheLengthOfCrossLength() { var rect = new OrientedBox(Vector.Zero, new Vector(3, 4), 10); CheckEqual(0, rect.SideOffset.DotProduct(rect.CrossOffset)); CheckEqual(10, rect.CrossOffset.Length()); } public void EmptyBox_IsZero() { CheckEqual(Vector.Zero, new OrientedBox(Vector.Zero, Vector.Zero, 5).CrossOffset); } } public class BoundingBoxTests : TestFixture { public void UprightBox_IsTheRect() { CheckEqual(RectAt(1, 2, 30, 40), OrientedBox.FromRect(RectAt(1, 2, 30, 40)).BoundingBox); } public void NegativeCrossLength_IsTheSameArea() { var rect = new OrientedBox(new Vector(1, 42), new Vector(30, 0), -40); CheckEqual(RectAt(1, 2, 30, 40), rect.BoundingBox); } public void EighthTurn_ReachesTheExtremeCornersAndNoFarther() { double diagonal = Math.Sqrt(0.5); Rect bounds = OrientedBox.FromRect(RectAt(0, 0, 100, 20)).Transformed(Turn(Math.PI / 4)).BoundingBox; CheckEqual(new Vector(-20 * diagonal, 0), bounds.TopLeft); CheckEqual(new Vector(100 * diagonal, 120 * diagonal), bounds.BottomRight); } } public class ContainsPointTests : TestFixture { public void UprightBox_ContainsItsInsideAndEdges() { OrientedBox rect = RectAt(0, 0, 10, 20); CheckTrue(rect.ContainsPoint(new Vector(5, 10))); CheckTrue(rect.ContainsPoint(new Vector(0, 0))); CheckTrue(rect.ContainsPoint(new Vector(10, 20))); } public void UprightBox_ExcludesPointsPastEachSide() { OrientedBox rect = RectAt(0, 0, 10, 20); CheckFalse(rect.ContainsPoint(new Vector(-1, 10))); CheckFalse(rect.ContainsPoint(new Vector(11, 10))); CheckFalse(rect.ContainsPoint(new Vector(5, -1))); CheckFalse(rect.ContainsPoint(new Vector(5, 21))); } public void NegativeCrossLength_ContainsTheSideItRunsToward() { var rect = new OrientedBox(Vector.Zero, new Vector(10, 0), -5); CheckTrue(rect.ContainsPoint(new Vector(5, -2))); CheckFalse(rect.ContainsPoint(new Vector(5, 2))); } public void TurnedBox_ContainsWhatItsCornersEnclose() { OrientedBox rect = OrientedBox.FromRect(RectAt(0, 0, 100, 20)).Transformed(Turn(Math.PI / 4)); CheckTrue(rect.ContainsPoint(Turn(Math.PI / 4).TransformPoint(new Vector(50, 10)))); CheckFalse(rect.ContainsPoint(Turn(Math.PI / 4).TransformPoint(new Vector(50, 30)))); CheckFalse(rect.ContainsPoint(Turn(Math.PI / 4).TransformPoint(new Vector(110, 10)))); } public void TurnedBox_ExcludesTheCornersOfItsBoundingBox() { OrientedBox rect = OrientedBox.FromRect(RectAt(0, 0, 100, 20)).Transformed(Turn(Math.PI / 4)); CheckFalse(rect.ContainsPoint(rect.BoundingBox.TopRight)); CheckFalse(rect.ContainsPoint(rect.BoundingBox.BottomLeft)); } public void EmptyBox_ContainsNothing() { CheckFalse(new OrientedBox(Vector.Zero, new Vector(10, 0), 0).ContainsPoint(new Vector(5, 0))); } } public class TryGetRectTests : TestFixture { public void UprightBox_ReturnsTheRect() { CheckTrue(OrientedBox.FromRect(RectAt(1, 2, 30, 40)).TryGetRect(out Rect rect)); CheckEqual(RectAt(1, 2, 30, 40), rect); } public void NegativeCrossLength_ReturnsTheSameArea() { var oriented = new OrientedBox(new Vector(1, 42), new Vector(30, 0), -40); CheckTrue(oriented.TryGetRect(out Rect rect)); CheckEqual(RectAt(1, 2, 30, 40), rect); } public void QuarterTurn_ReturnsTheTurnedArea() { OrientedBox turned = OrientedBox.FromRect(RectAt(0, 0, 10, 20)).Transformed(Turn(Math.PI / 2)); CheckTrue(turned.TryGetRect(out Rect rect)); CheckEqual(turned.BoundingBox, rect); CheckEqual(new Vector(20, 10), rect.Size); } public void MirroredBox_ReturnsTheMirroredArea() { OrientedBox mirrored = OrientedBox.FromRect(RectAt(1, 2, 30, 40)) .Transformed(AffineMatrix.BuildFromScale(new Vector(-1, 1))); CheckTrue(mirrored.TryGetRect(out Rect rect)); CheckEqual(RectAt(-31, 2, 30, 40), rect); } public void QuarterTurnTheOtherWay_ReturnsTheTurnedArea() { OrientedBox turned = OrientedBox.FromRect(RectAt(0, 0, 10, 20)).Transformed(Turn(-Math.PI / 2)); CheckTrue(turned.TryGetRect(out Rect rect)); CheckEqual(turned.BoundingBox, rect); } public void NegativeCrossLengthOnAQuarterTurn_ReturnsTheTurnedArea() { var upright = new OrientedBox(Vector.Zero, new Vector(10, 0), -20); OrientedBox turned = upright.Transformed(Turn(Math.PI / 2)); CheckTrue(turned.TryGetRect(out Rect rect)); CheckEqual(turned.BoundingBox, rect); } public void NoSideOffset_ReturnsNothingAtTheOrigin() { var empty = new OrientedBox(new Vector(3, 4), Vector.Zero, 5); CheckTrue(empty.TryGetRect(out Rect rect)); CheckEqual(RectAt(3, 4, 0, 0), rect); } public void EighthTurn_ReturnsFalse() { OrientedBox turned = OrientedBox.FromRect(RectAt(0, 0, 10, 20)).Transformed(Turn(Math.PI / 4)); CheckFalse(turned.TryGetRect(out Rect rect)); } } public class TranslatedTests : TestFixture { public void MovesOnlyTheOrigin() { var box = new OrientedBox(new Vector(1, 2), new Vector(3, 4), -5); CheckEqual( new OrientedBox(new Vector(11, 22), new Vector(3, 4), -5), box.Translated(new Vector(10, 20))); } public void ATurnedBox_KeepsItsSidesAndMovesItsOrigin() { OrientedBox turned = OrientedBox.FromRect(RectAt(1, 2, 30, 40)).Transformed(Turn(0.7)); OrientedBox moved = turned.Translated(new Vector(-6, 9)); CheckEqual(new OrientedBox(turned.Origin + new Vector(-6, 9), turned.SideOffset, turned.CrossLength), moved); } } public class ScaledAboutOriginTests : TestFixture { public void ScalesTheOriginAndBothSides() { OrientedBox box = OrientedBox.FromRect(RectAt(1, 2, 30, 40)); CheckEqual(OrientedBox.FromRect(RectAt(2, 4, 60, 80)), box.ScaledAboutOrigin(2)); } public void ATurnedBox_MatchesTransformingByTheScale() { OrientedBox turned = OrientedBox.FromRect(RectAt(1, 2, 30, 40)).Transformed(Turn(0.7)); CheckEqual(turned.Transformed(AffineMatrix.BuildFromScale(0.25)), turned.ScaledAboutOrigin(0.25)); } public void AMirroredBox_KeepsItsCrossLengthSign() { var mirrored = new OrientedBox(new Vector(1, 2), new Vector(3, 4), -5); CheckEqual(mirrored.Transformed(AffineMatrix.BuildFromScale(3)), mirrored.ScaledAboutOrigin(3)); } public void ANegativeFactor_MatchesTransformingByTheScale() { OrientedBox box = OrientedBox.FromRect(RectAt(1, 2, 30, 40)); CheckEqual(box.Transformed(AffineMatrix.BuildFromScale(-2)), box.ScaledAboutOrigin(-2)); } } public class TransformedTests : TestFixture { public void Identity_IsTheSameBox() { OrientedBox rect = new OrientedBox(new Vector(1, 2), new Vector(3, 4), -5); CheckEqual(rect, rect.Transformed(AffineMatrix.Identity)); } public void Translation_MovesOnlyTheOrigin() { OrientedBox moved = OrientedBox.FromRect(RectAt(1, 2, 30, 40)) .Transformed(AffineMatrix.BuildFromTranslation(new Vector(10, 20))); CheckEqual(OrientedBox.FromRect(RectAt(11, 22, 30, 40)), moved); } public void TranslationOfATurnedBox_KeepsItsSides() { OrientedBox turned = OrientedBox.FromRect(RectAt(1, 2, 30, 40)).Transformed(Turn(0.7)); OrientedBox moved = turned.Transformed(AffineMatrix.BuildFromTranslation(new Vector(5, -3))); CheckEqual(turned.Origin + new Vector(5, -3), moved.Origin); CheckEqual(turned.SideOffset, moved.SideOffset); CheckEqual(turned.CrossLength, moved.CrossLength); } public void UniformScale_ScalesBothSides() { OrientedBox scaled = OrientedBox.FromRect(RectAt(1, 2, 30, 40)) .Transformed(AffineMatrix.BuildFromScale(new Vector(2, 2))); CheckEqual(OrientedBox.FromRect(RectAt(2, 4, 60, 80)), scaled); } public void ScaleOfAnUprightBox_StretchesEachAxisOnItsOwn() { OrientedBox scaled = OrientedBox.FromRect(RectAt(1, 2, 30, 40)) .Transformed(AffineMatrix.BuildFromScale(new Vector(2, 0.5))); CheckEqual(OrientedBox.FromRect(RectAt(2, 1, 60, 20)), scaled); } public void Turn_CarriesEachCornerToTheTurnedCorner() { AffineMatrix turn = Turn(Math.PI / 6); Rect source = RectAt(1, 2, 30, 40); OrientedBox turned = OrientedBox.FromRect(source).Transformed(turn); CheckEqual(turn.TransformPoint(source.TopLeft), turned.Origin); CheckEqual(turn.TransformPoint(source.TopRight), turned.Origin + turned.SideOffset); CheckEqual(turn.TransformPoint(source.BottomLeft), turned.Origin + turned.CrossOffset); CheckEqual(40, turned.CrossLength); } public void Reflection_FlipsWhichSideCrossLengthRunsTo() { OrientedBox reflected = OrientedBox.FromRect(RectAt(0, 0, 10, 5)) .Transformed(AffineMatrix.BuildFromScale(new Vector(-1, 1))); CheckEqual(new Vector(-10, 0), reflected.SideOffset); CheckEqual(-5, reflected.CrossLength); CheckEqual(new Vector(0, 5), reflected.CrossOffset); } public void TurnedBoxUnderANonUniformScale_Throws() { OrientedBox turned = OrientedBox.FromRect(RectAt(0, 0, 10, 20)).Transformed(Turn(Math.PI / 6)); CheckThrow(typeof(ArgumentException)); turned.Transformed(AffineMatrix.BuildFromScale(new Vector(2, 1))); } public void UprightBoxUnderAShear_Throws() { OrientedBox upright = RectAt(0, 0, 10, 20); var shear = new AffineMatrix(new Vector(1, 0), new Vector(1, 1), Vector.Zero); CheckThrow(typeof(ArgumentException)); upright.Transformed(shear); } public void ScaleToNothingOnOneAxis_Throws() { OrientedBox upright = RectAt(0, 0, 10, 20); CheckThrow(typeof(ArgumentException)); upright.Transformed(AffineMatrix.BuildFromScale(new Vector(1, 0))); } public void EmptyBox_PassesThroughAnyMatrix() { var empty = new OrientedBox(Vector.Zero, new Vector(10, 0), 0); var shear = new AffineMatrix(new Vector(1, 0), new Vector(1, 1), Vector.Zero); CheckTrue(empty.Transformed(shear).IsEmpty); } } public class IntersectionTests : TestFixture { public void UprightBoxes_ShareWhatRectIntersectionFinds() { OrientedBox? shared = OrientedBox.FromRect(RectAt(0, 0, 10, 10)).Intersection(RectAt(5, 3, 10, 10)); CheckEqual(OrientedBox.FromRect(RectAt(5, 3, 5, 7)), shared.Value); } public void BoxesThatShareNothing_ReturnNull() { CheckFalse(OrientedBox.FromRect(RectAt(0, 0, 10, 10)).Intersection(RectAt(20, 20, 5, 5)).HasValue); } public void NegativeCrossLength_SharesTheSameArea() { var above = new OrientedBox(Vector.Zero, new Vector(10, 0), -5); OrientedBox? shared = above.Intersection(RectAt(0, -3, 10, 10)); CheckEqual(OrientedBox.FromRect(RectAt(0, -3, 10, 3)), shared.Value); } public void BoxesTurnedTogether_ShareTheTurnedOverlap() { AffineMatrix turn = Turn(Math.PI / 6); OrientedBox first = OrientedBox.FromRect(RectAt(0, 0, 100, 50)).Transformed(turn); OrientedBox second = OrientedBox.FromRect(RectAt(50, 20, 100, 50)).Transformed(turn); OrientedBox? shared = first.Intersection(second); CheckEqual(OrientedBox.FromRect(RectAt(50, 20, 50, 30)).Transformed(turn), shared.Value); } public void BoxesAQuarterTurnApart_ShareTheirOverlap() { OrientedBox upright = RectAt(0, 0, 10, 10); var quarterAndRight = new AffineMatrix(Math.PI / 2, new Vector(5, 0)); OrientedBox quarter = OrientedBox.FromRect(RectAt(0, 0, 4, 20)).Transformed(quarterAndRight); OrientedBox? shared = upright.Intersection(quarter); CheckEqual(RectAt(0, 0, 5, 4), shared.Value.BoundingBox); } public void BoxesTiltedAgainstEachOther_Throw() { OrientedBox upright = RectAt(0, 0, 10, 10); OrientedBox tilted = upright.Transformed(Turn(Math.PI / 6)); CheckThrow(typeof(NotSupportedException)); upright.Intersection(tilted); } public void EmptyBoxes_ReturnNull() { var empty = new OrientedBox(Vector.Zero, new Vector(10, 0), 0); CheckFalse(empty.Intersection(RectAt(0, 0, 10, 10)).HasValue); CheckFalse(OrientedBox.FromRect(RectAt(0, 0, 10, 10)).Intersection(empty).HasValue); } } public class AreaTests : TestFixture { public void Always_IsTheProductOfTheSides() { CheckEqual(12, new OrientedBox(Vector.Zero, new Vector(0, 3), 4).Area); } public void NegativeCrossLength_IsStillPositive() { CheckEqual(12, new OrientedBox(Vector.Zero, new Vector(3, 0), -4).Area); } } public class InscribedIntersectionTests : TestFixture { public void UprightBoxes_ShareExactlyWhatIntersectionFinds() { OrientedBox first = RectAt(0, 0, 10, 10); OrientedBox? shared = first.InscribedIntersection(RectAt(5, 3, 10, 10)); CheckEqual(OrientedBox.FromRect(RectAt(5, 3, 5, 7)), shared.Value); } public void BoxesTurnedTogether_ShareExactlyWhatIntersectionFinds() { AffineMatrix turn = Turn(Math.PI / 6); OrientedBox first = OrientedBox.FromRect(RectAt(0, 0, 100, 50)).Transformed(turn); OrientedBox second = OrientedBox.FromRect(RectAt(50, 20, 100, 50)).Transformed(turn); OrientedBox? shared = first.InscribedIntersection(second); CheckEqual(first.Intersection(second).Value, shared.Value); } public void TurnedBoxInsideAnUprightOne_IsTheTurnedBox() { OrientedBox big = RectAt(-1000, -1000, 3000, 3000); OrientedBox small = OrientedBox.FromRect(RectAt(0, 0, 10, 20)).Transformed(Turn(Math.PI / 6)); CheckEqual(small, big.InscribedIntersection(small).Value); CheckEqual(small, small.InscribedIntersection(big).Value); } public void UprightBoxInsideATurnedOne_IsTheUprightBox() { OrientedBox big = OrientedBox.FromRect(RectAt(-1000, -1000, 3000, 3000)).Transformed(Turn(Math.PI / 6)); OrientedBox small = RectAt(0, 0, 10, 20); CheckEqual(small, big.InscribedIntersection(small).Value); CheckEqual(small, small.InscribedIntersection(big).Value); } public void BoxesCrossingEachOthersEdges_FitInsideBoth() { OrientedBox upright = RectAt(0, 0, 100, 100); OrientedBox tilted = OrientedBox.FromRect(RectAt(0, 0, 100, 100)) .Transformed(new AffineMatrix(Math.PI / 6, new Vector(50, 0))); OrientedBox? shared = upright.InscribedIntersection(tilted); CheckTrue(shared.Value.Area > 0); foreach (Vector corner in Corners(shared.Value)) { CheckTrue(upright.ContainsPoint(corner)); CheckTrue(tilted.ContainsPoint(corner)); } } public void NegativeCrossLength_SharesTheSameAreaAsIntersection() { var above = new OrientedBox(Vector.Zero, new Vector(10, 0), -5); OrientedBox? shared = above.InscribedIntersection(RectAt(0, -3, 10, 10)); CheckEqual(OrientedBox.FromRect(RectAt(0, -3, 10, 3)), shared.Value); } public void BoxesThatShareNothing_ReturnNull() { OrientedBox tilted = OrientedBox.FromRect(RectAt(0, 0, 10, 10)).Transformed(Turn(Math.PI / 6)); CheckFalse(OrientedBox.FromRect(RectAt(100, 100, 10, 10)).InscribedIntersection(tilted).HasValue); CheckFalse(tilted.InscribedIntersection(RectAt(100, 100, 10, 10)).HasValue); } public void BoxesThatOnlyTouch_ReturnNull() { OrientedBox left = RectAt(0, 0, 10, 10); CheckFalse(left.InscribedIntersection(RectAt(10, 0, 5, 10)).HasValue); } public void EmptyBoxes_ReturnNull() { var empty = new OrientedBox(Vector.Zero, new Vector(10, 0), 0); CheckFalse(empty.InscribedIntersection(RectAt(0, 0, 10, 10)).HasValue); CheckFalse(OrientedBox.FromRect(RectAt(0, 0, 10, 10)).InscribedIntersection(empty).HasValue); } } public class ToStringTests : TestFixture { public void Always_NamesTheThreeParts() { var rect = new OrientedBox(new Vector(1, 2), new Vector(3, 4), 5); CheckEqual($"{rect.Origin} + {rect.SideOffset} x {rect.CrossLength}", rect.ToString()); } } } }