using System; using System.Collections.Generic; using System.Linq; using Azimuth; using Blacklight.Core.Drawables; namespace Blacklight.Core.UnitTests.Drawables { public class ClosedQuadraticBezierCurveTests : UnitTestSharp.TestFixture { // Passthrough points (-1, 0), (1, 0), (0, -3), each piece bulging outward. public static readonly Vector[] BulgingOut = { new Vector(-1, 0), new Vector(0, 1), new Vector(1, 0), new Vector(1.5, -1), new Vector(0, -3), new Vector(-1.5, -1), }; // As BulgingOut, but the top and right pieces bow in. public static readonly Vector[] BowingIn = { new Vector(-1, 0), new Vector(0, -0.5), new Vector(1, 0), new Vector(0.4, -1), new Vector(0, -3), new Vector(-1.5, -1), }; public class CtorTests : UnitTestSharp.TestFixture { public void OddVertexCount_Throws() { CheckThrow(typeof(ArgumentException)); new ClosedQuadraticBezierCurve(BulgingOut.Take(5).ToList()); } public void FewerThanFourVertices_Throws() { CheckThrow(typeof(ArgumentException)); new ClosedQuadraticBezierCurve(BulgingOut.Take(2).ToList()); } public void EachPiece_BecomesOneTriangle() { var curve = new ClosedQuadraticBezierCurve(BulgingOut); CheckEqual(new[] { new Vector(-1, 0), new Vector(0, 1), new Vector(1, 0), new Vector(1, 0), new Vector(1.5, -1), new Vector(0, -3), new Vector(0, -3), new Vector(-1.5, -1), new Vector(-1, 0), }, curve.BezierTriangles); CheckEqual(new[] { false, false, false }, curve.BezierTrianglesInverted); } public void BowingPieces_AreInverted() { var curve = new ClosedQuadraticBezierCurve(BowingIn); CheckEqual(new[] { true, true, false }, curve.BezierTrianglesInverted); } public void CounterclockwiseCurve_FindsTheSameBowingPieces() { var curve = new ClosedQuadraticBezierCurve(new[] { BowingIn[0], BowingIn[5], BowingIn[4], BowingIn[3], BowingIn[2], BowingIn[1], }); CheckEqual(new[] { false, true, true }, curve.BezierTrianglesInverted); } public void ControlPointBelowAnotherPassthroughPoint_StillBulges() { var curve = new ClosedQuadraticBezierCurve(new[] { new Vector(-1, 0), new Vector(0, 1), new Vector(1, 0), new Vector(1, -1), new Vector(0, -3), new Vector(-1, -1), }); CheckEqual(new[] { false, false, false }, curve.BezierTrianglesInverted); } } public static bool AreAdjacent(IList polygon, Vector a, Vector b) { for (int i = 0; i < polygon.Count; ++i) { Vector next = polygon[(i + 1) % polygon.Count]; if ((polygon[i].Equals(a) && next.Equals(b)) || (polygon[i].Equals(b) && next.Equals(a))) { return true; } } return false; } public static double TotalArea(IList> regions) { return regions.Sum(region => Math.Abs((double)Annulus.Polygon.Utilities.SignedArea(region))); } public class PieceRegionsTests : UnitTestSharp.TestFixture { public void EachPiece_OwnsTheRegionAlongItsChord() { var curve = new ClosedQuadraticBezierCurve(BulgingOut); CheckEqual(3, curve.PieceRegions.Count); CheckTrue(AreAdjacent(curve.PieceRegions[0], new Vector(-1, 0), new Vector(1, 0))); CheckTrue(AreAdjacent(curve.PieceRegions[1], new Vector(1, 0), new Vector(0, -3))); CheckTrue(AreAdjacent(curve.PieceRegions[2], new Vector(0, -3), new Vector(-1, 0))); } public void BulgingPieces_TileThePassthroughPolygon() { var curve = new ClosedQuadraticBezierCurve(BulgingOut); CheckLess(Math.Abs(3 - TotalArea(curve.PieceRegions)), 1e-9); } public void BowingPiece_RoutesItsRegionThroughItsControlPoint() { var curve = new ClosedQuadraticBezierCurve(BowingIn); CheckTrue(AreAdjacent(curve.PieceRegions[0], new Vector(-1, 0), new Vector(0, -0.5))); CheckTrue(AreAdjacent(curve.PieceRegions[0], new Vector(0, -0.5), new Vector(1, 0))); CheckTrue(AreAdjacent(curve.PieceRegions[2], new Vector(0, -3), new Vector(-1, 0))); CheckFalse(curve.PieceRegions[2].Contains(new Vector(-1.5, -1))); } public void BowingPieces_LeaveTheirControlTrianglesOut() { var curve = new ClosedQuadraticBezierCurve(BowingIn); // The passthrough triangle's 3, less the bowing pieces' control triangles of 0.5 and 0.4. CheckLess(Math.Abs(2.1 - TotalArea(curve.PieceRegions)), 1e-9); } public void TwoPieces_HaveNone() { var curve = new ClosedQuadraticBezierCurve(new[] { new Vector(-1, 0), new Vector(0, 1), new Vector(1, 0), new Vector(0, -1), }); CheckEqual(0, curve.PieceRegions.Count); } public void CollinearPassthroughPoints_StillGetOneRegionEach() { var curve = new ClosedQuadraticBezierCurve(new[] { new Vector(-1, -1), new Vector(-0.5, -1.1), new Vector(0, -1), new Vector(0.5, -1.1), new Vector(1, -1), new Vector(1.1, -0.5), new Vector(1, 0), new Vector(1.1, 0.5), new Vector(1, 1), new Vector(0.5, 1.1), new Vector(0, 1), new Vector(-0.5, 1.1), new Vector(-1, 1), new Vector(-1.1, 0.5), new Vector(-1, 0), new Vector(-1.1, -0.5), }); CheckEqual(8, curve.PieceRegions.Count); CheckLess(Math.Abs(4 - TotalArea(curve.PieceRegions)), 1e-9); } } public static double TrianglesArea(IList corners) { double returnMe = 0; for (int i = 0; i < corners.Count; i += 3) { Scalar doubled = Vector.CrossProduct(corners[i + 1] - corners[i], corners[i + 2] - corners[i]); returnMe += Math.Abs((double)doubled) / 2; } return returnMe; } public class PieceRegionTrianglesTests : UnitTestSharp.TestFixture { public void EachRegion_TriangulatesToExactlyItsArea() { var curve = new ClosedQuadraticBezierCurve(BowingIn); CheckEqual(3, curve.PieceRegionTriangles.Count); for (int piece = 0; piece < 3; ++piece) { IList corners = curve.PieceRegionTriangles[piece]; CheckEqual(0, corners.Count % 3); Scalar signedArea = Annulus.Polygon.Utilities.SignedArea(curve.PieceRegions[piece]); double regionArea = Math.Abs((double)signedArea); CheckLess(Math.Abs(regionArea - TrianglesArea(corners)), 1e-9); } } public void TwoPieces_HaveNone() { var curve = new ClosedQuadraticBezierCurve(new[] { new Vector(-1, 0), new Vector(0, 1), new Vector(1, 0), new Vector(0, -1), }); CheckEqual(0, curve.PieceRegionTriangles.Count); } } } }