using System; using System.Collections.Generic; using System.Linq; using Azimuth; using UnitTestSharp; namespace Blacklight.Bezier.UnitTests { public class CubicSampleSetTests : TestFixture { public class CtorTests : TestFixture { public void AssignsBothFields() { var samples = new CubicSample[2]; var set = new CubicSampleSet("Name", samples); CheckEqual("Name", set.Name); CheckEqual(samples, set.Samples); } } } public class CubicSampleSetsTests : TestFixture { public const int Draws = 20; public static readonly CubicPiece Straight = CubicSample.PieceInFrame(new Vector(0.3, 0), new Vector(0.6, 0)); public static bool SameBits(Vector first, Vector second) { return first.X.Value == second.X.Value && first.Y.Value == second.Y.Value; } /// /// Returns the parameter, on a fine grid, where the piece moves slowest. /// public static double SlowestParameter(CubicPiece piece) { double returnMe = 0; double slowest = double.PositiveInfinity; for (int i = 0; i <= 100000; i++) { double t = i / 100000.0; double speed = piece.DerivativeAt(t).Length(); if (speed < slowest) { slowest = speed; returnMe = t; } } return returnMe; } public class SizeTests : TestFixture { public void SmallPiece_IsOne() { CheckEqual(1.0, CubicSampleSets.Size(CubicSample.PieceInFrame(new Vector(0.3, 0.4), Vector.Zero))); } public void LargeInnerPoint_IsItsLength() { CheckEqual(5.0, CubicSampleSets.Size(CubicSample.PieceInFrame(Vector.Zero, new Vector(3, 4)))); } } public class RandomDirectionTests : TestFixture { public void IsUnitLength() { var random = new Random(1); for (int i = 0; i < Draws; i++) { CheckGreater(1e-15, Math.Abs(CubicSampleSets.RandomDirection(random).Length() - 1)); } } } public class RandomControlPointTests : TestFixture { public void StaysInTheBoxAroundTheFrame() { var random = new Random(1); for (int i = 0; i < Draws; i++) { Vector point = CubicSampleSets.RandomControlPoint(random); CheckTrue(point.X.Value >= -1 && point.X.Value <= 2); CheckTrue(point.Y.Value >= -1.5 && point.Y.Value <= 1.5); } } } public class DrawTests : TestFixture { public void ReturnsTheFirstAcceptedPiece() { CubicPiece drawn = CubicSampleSets.Draw(new Random(1), piece => true); var random = new Random(1); CheckEqual(CubicSampleSets.RandomControlPoint(random), drawn.P1); CheckEqual(CubicSampleSets.RandomControlPoint(random), drawn.P2); } public void NothingAccepted_Throws() { CheckThrow(typeof(InvalidOperationException)); CubicSampleSets.Draw(new Random(1), piece => false); } } public class HasLoopTests : TestFixture { public void Loop_IsTrue() { CheckTrue(CubicSampleSets.HasLoop(CubicReferenceDistanceTests.Loop)); } public void Arch_IsFalse() { CheckFalse(CubicSampleSets.HasLoop(CubicFeaturesTests.Arch)); } } public class ArchTests : TestFixture { public void HasNoInflectionOrLoop() { var random = new Random(1); for (int i = 0; i < Draws; i++) { CubicPiece piece = CubicSampleSets.Arch(random); CheckEqual(0, CubicFeatures.Inflections(piece).Count); CheckFalse(CubicSampleSets.HasLoop(piece)); } } } public class OneInflectionTests : TestFixture { public void HasOneInflectionAndNoLoop() { var random = new Random(1); for (int i = 0; i < Draws; i++) { CubicPiece piece = CubicSampleSets.OneInflection(random); CheckEqual(1, CubicFeatures.Inflections(piece).Count); CheckFalse(CubicSampleSets.HasLoop(piece)); } } } public class TwoInflectionsTests : TestFixture { public void HasTwoInflections() { var random = new Random(1); for (int i = 0; i < Draws; i++) { CheckEqual(2, CubicFeatures.Inflections(CubicSampleSets.TwoInflections(random)).Count); } } } public class LoopTests : TestFixture { public void CrossesItself() { var random = new Random(1); for (int i = 0; i < Draws; i++) { CheckTrue(CubicSampleSets.HasLoop(CubicSampleSets.Loop(random))); } } } public class CuspPieceTests : TestFixture { public void StopsAtTheCusp() { CubicPiece piece = CubicSampleSets.CuspPiece(0.3, new Vector(0.5, 1)); CheckGreater(1e-14, piece.DerivativeAt(0.3).Length().Value); } public void KeepsTheFrameAndTheFirstInnerPoint() { CubicPiece piece = CubicSampleSets.CuspPiece(0.3, new Vector(0.5, 1)); CheckEqual(Vector.Zero, piece.P0); CheckEqual(new Vector(0.5, 1), piece.P1); CheckEqual(Vector.UnitX, piece.P3); } } public class CuspTests : TestFixture { public void StopsSomewhereInTheMiddle() { var random = new Random(1); for (int i = 0; i < 3; i++) { CubicPiece piece = CubicSampleSets.Cusp(random); double cuspT = SlowestParameter(piece); CheckTrue(cuspT >= 0.1 && cuspT <= 0.6); CheckGreater(1e-3 * CubicSampleSets.Size(piece), piece.DerivativeAt(cuspT).Length().Value); } } } public class NearCuspTests : TestFixture { public void IsACuspWithItsSecondInnerPointNudged() { for (int seed = 0; seed < Draws; seed++) { CubicPiece cusp = CubicSampleSets.Cusp(new Random(seed)); CubicPiece nearCusp = CubicSampleSets.NearCusp(new Random(seed)); double nudge = (nearCusp.P2 - cusp.P2).Length(); CheckEqual(cusp.P1, nearCusp.P1); CheckTrue(nudge >= 1e-9 && nudge <= 1e-2); } } } public class NearQuadraticTests : TestFixture { public void HasASmallButNonzeroCubicTerm() { var random = new Random(1); for (int i = 0; i < Draws; i++) { CubicFeatures.PowerBasis(CubicSampleSets.NearQuadratic(random), out Vector cubic, out _, out _); CheckTrue(cubic.Length() > 0 && cubic.Length() <= 6e-2); } } } public class NearLineTests : TestFixture { public void InnerPointsSitJustOffTheChord() { var random = new Random(1); for (int i = 0; i < Draws; i++) { CubicPiece piece = CubicSampleSets.NearLine(random); CheckTrue(Math.Abs(piece.P1.Y) >= 1e-9 && Math.Abs(piece.P1.Y) <= 1e-2); CheckTrue(Math.Abs(piece.P2.Y) >= 1e-9 && Math.Abs(piece.P2.Y) <= 1e-2); } } } public class NearlyClosedTests : TestFixture { public void InnerPointsDwarfTheChord() { var random = new Random(1); for (int i = 0; i < Draws; i++) { CubicPiece piece = CubicSampleSets.NearlyClosed(random); CheckTrue(piece.P1.Length() >= 10 && piece.P2.Length() >= 10); } } } public class AlongNormalTests : TestFixture { public void IsThatFarFromTheCurveWithItsFootThere() { CubicPiece piece = CubicPieceTests.Wave; CubicReferenceResult result = CubicReferenceDistance.Solve(piece, CubicSampleSets.AlongNormal(piece, t: 0.3, offset: 1e-3)); CheckGreater(1e-15, Math.Abs(result.Distance - 1e-3)); CheckGreater(1e-9, Math.Abs(result.ClosestT - 0.3)); } public void PositiveOffset_IsToTheLeftOfTravel() { Vector pixel = CubicSampleSets.AlongNormal(CubicFeaturesTests.UpwardParabola, t: 0.5, offset: 2); CheckGreater(1e-14, (pixel - new Vector(0, 2)).Length().Value); } } public class NearCurveTests : TestFixture { public void IsNoFartherThanThePiecesSize() { var random = new Random(1); CubicPiece piece = CubicPieceTests.Wave; for (int i = 0; i < Draws; i++) { double distance = CubicReferenceDistance.Solve(piece, CubicSampleSets.NearCurve(random, piece)) .Distance; CheckTrue(distance <= CubicSampleSets.Size(piece)); } } } public class NearEvoluteTests : TestFixture { public void LandsJustOffTheCentreOfCurvatureAtItsDrawnParameter() { CubicPiece piece = CubicPieceTests.Wave; for (int seed = 0; seed < Draws; seed++) { Vector pixel = CubicSampleSets.NearEvolute(new Random(seed), piece); double t = SampleSets.Uniform(new Random(seed), 0, 1); Vector centre = CubicFeatures.CentreOfCurvature(piece, t); double radius = (centre - piece.PointAt(t)).Length(); CheckGreater(1e-2 * radius, (pixel - centre).Length().Value); } } public void Straight_Throws() { CheckThrow(typeof(InvalidOperationException)); CubicSampleSets.NearEvolute(new Random(1), Straight); } } public class NearEndsTests : TestFixture { public void IsWithinThePiecesSizeOfAnEnd() { var random = new Random(1); CubicPiece piece = CubicPieceTests.Wave; for (int i = 0; i < Draws; i++) { Vector pixel = CubicSampleSets.NearEnds(random, piece); double nearerEnd = Math.Min((pixel - piece.P0).Length(), (pixel - piece.P3).Length()); CheckTrue(nearerEnd <= CubicSampleSets.Size(piece)); } } public void VisitsBothEnds() { var random = new Random(1); CubicPiece piece = CubicPieceTests.Wave; int nearStart = 0; for (int i = 0; i < Draws; i++) { Vector pixel = CubicSampleSets.NearEnds(random, piece); if ((pixel - piece.P0).Length() < (pixel - piece.P3).Length()) { nearStart++; } } CheckTrue(nearStart > 0 && nearStart < Draws); } } public class WholePlaneTests : TestFixture { public void SpreadsFromNearTheMiddleToFarAway() { var random = new Random(1); CubicPiece piece = CubicPieceTests.Wave; double size = CubicSampleSets.Size(piece); for (int i = 0; i < Draws; i++) { double radius = (CubicSampleSets.WholePlane(random, piece) - piece.PointAt(0.5)).Length(); CheckTrue(radius >= 1e-4 * size * (1 - 1e-12) && radius <= 1e6 * size * (1 + 1e-12)); } } } public class IsLocalMinimumTests : TestFixture { public void StartTheCurveLeavesFrom_IsOne() { CheckTrue(CubicSampleSets.IsLocalMinimum(Straight, new Vector(-1, 1), t: 0)); } public void StartTheCurveHeadsToward_IsNot() { CheckFalse(CubicSampleSets.IsLocalMinimum(Straight, new Vector(0.5, 1), t: 0)); } public void EndTheCurveArrivesAwayFrom_IsOne() { CheckTrue(CubicSampleSets.IsLocalMinimum(Straight, new Vector(2, 1), t: 1)); } public void EndTheCurveArrivesFrom_IsNot() { CheckFalse(CubicSampleSets.IsLocalMinimum(Straight, new Vector(0.5, 1), t: 1)); } public void Interior_IsOne() { CheckTrue(CubicSampleSets.IsLocalMinimum(Straight, new Vector(0.5, 1), t: 0.5)); } } public class OwnershipBoundaryTests : TestFixture { public void TwoBranchesTieThere() { var random = new Random(1); CubicPiece piece = CubicReferenceDistanceTests.Loop; for (int i = 0; i < Draws; i++) { Vector pixel = CubicSampleSets.OwnershipBoundary(random, piece, CubicSampleSets.AttemptLimit); var parameters = new List { 0, 1 }; CubicReferenceDistance.FootCandidates(CubicReferenceDistance.FootCoefficients(piece, pixel), parameters, CubicReferenceDistance.SmallestInterval, CubicReferenceDistance.IntervalBudget); double closest = parameters.Min(t => CubicReferenceDistance.SquaredDistanceAt(piece, pixel, t)); List tied = parameters .Where(t => CubicReferenceDistance.SquaredDistanceAt(piece, pixel, t) <= closest * (1 + 1e-6)) .ToList(); CheckGreater(tied.Max() - tied.Min(), CubicSampleSets.BranchGap); } } public void NoCompetingBranches_Throws() { CheckThrow(typeof(InvalidOperationException)); CubicSampleSets.OwnershipBoundary(new Random(1), Straight, attemptLimit: 10); } } public class NearCuspPointTests : TestFixture { public void IsCloseToTheCusp() { var random = new Random(1); for (int i = 0; i < 3; i++) { CubicSample sample = CubicSampleSets.NearCuspPoint(random); CubicPiece piece = sample.ToPiece(); double cuspT = SlowestParameter(piece); double size = CubicSampleSets.Size(piece); CheckGreater(1e-3 * size, piece.DerivativeAt(cuspT).Length().Value); CheckGreater(0.1 * size + 1e-3, (sample.Pixel - piece.PointAt(cuspT)).Length().Value); } } } public class NearDoublePointTests : TestFixture { public void IsCloseToWhereTheLoopCrosses() { var random = new Random(1); for (int i = 0; i < Draws; i++) { CubicSample sample = CubicSampleSets.NearDoublePoint(random); CubicPiece piece = sample.ToPiece(); CheckTrue(CubicFeatures.SelfIntersection(piece, out double first, out _)); CheckTrue((sample.Pixel - piece.PointAt(first)).Length() <= 0.1 * CubicSampleSets.Size(piece)); } } } public class CreateTests : TestFixture { public void OneSetPerShapeAndPixelPlusTheFeatureSets() { List sets = CubicSampleSets.Create(samplesPerSet: 2, seed: 1); CheckEqual(9 * 5 + 2, sets.Count); CheckEqual(sets.Count, sets.Select(set => set.Name).Distinct().Count()); CheckTrue(sets.All(set => set.Samples.Length == 2)); } public void SameSeed_GivesTheSameSamples() { List first = CubicSampleSets.Create(samplesPerSet: 2, seed: 1); List second = CubicSampleSets.Create(samplesPerSet: 2, seed: 1); for (int setIndex = 0; setIndex < first.Count; setIndex++) { for (int i = 0; i < 2; i++) { CubicSample expected = first[setIndex].Samples[i]; CubicSample actual = second[setIndex].Samples[i]; CheckTrue(SameBits(expected.Pixel, actual.Pixel) && SameBits(expected.P1, actual.P1) && SameBits(expected.P2, actual.P2)); } } } } } }