using System; using System.Collections.Generic; using System.Linq; using Azimuth; using UnitTestSharp; namespace Blacklight.Bezier.UnitTests { public class CubicReferenceResultTests : TestFixture { public class CtorTests : TestFixture { public void AssignsBothFields() { var result = new CubicReferenceResult(distance: 1, closestT: 0.5); CheckEqual(1.0, result.Distance); CheckEqual(0.5, result.ClosestT); } } } public class CubicReferenceDistanceTests : TestFixture { public static readonly CubicPiece Wave = CubicPieceTests.Wave; public static readonly CubicPiece Cusp = new CubicPiece(new Vector(0, 0), new Vector(1, 1), new Vector(0, 1), new Vector(1, 0)); public static readonly CubicPiece Loop = new CubicPiece(new Vector(0, 0), new Vector(3, 2), new Vector(-2, 2), new Vector(1, 0)); public static double Binomial(int n, int k) { double returnMe = 1; for (int i = 1; i <= k; i++) { returnMe = returnMe * (n - k + i) / i; } return returnMe; } /// /// Returns the Bernstein coefficients on [0, 1] of the monic polynomial with these roots. /// public static double[] BernsteinFromRoots(params double[] roots) { var monomial = new double[roots.Length + 1]; monomial[0] = 1; for (int r = 0; r < roots.Length; r++) { for (int i = r + 1; i > 0; i--) { monomial[i] = monomial[i - 1] - roots[r] * monomial[i]; } monomial[0] = -roots[r] * monomial[0]; } int degree = roots.Length; var returnMe = new double[degree + 1]; for (int k = 0; k <= degree; k++) { for (int i = 0; i <= k; i++) { returnMe[k] += Binomial(k, i) / Binomial(degree, i) * monomial[i]; } } return returnMe; } public static double BruteForceDistance(CubicPiece piece, Vector pixel) { const int gridSteps = 2000; int best = 0; double bestSquared = double.PositiveInfinity; for (int i = 0; i <= gridSteps; i++) { double squared = CubicReferenceDistance.SquaredDistanceAt(piece, pixel, (double)i / gridSteps); if (squared < bestSquared) { bestSquared = squared; best = i; } } double low = (double)Math.Max(best - 1, 0) / gridSteps; double high = (double)Math.Min(best + 1, gridSteps) / gridSteps; double inverseGolden = (Math.Sqrt(5) - 1) / 2; for (int i = 0; i < 100; i++) { double left = high - inverseGolden * (high - low); double right = low + inverseGolden * (high - low); if (CubicReferenceDistance.SquaredDistanceAt(piece, pixel, left) < CubicReferenceDistance.SquaredDistanceAt(piece, pixel, right)) { high = right; } else { low = left; } } return Math.Sqrt(Math.Min(bestSquared, CubicReferenceDistance.SquaredDistanceAt(piece, pixel, (low + high) / 2))); } public static Vector RandomPoint(Random random, double reach) { return new Vector(SampleSets.Uniform(random, -reach, reach), SampleSets.Uniform(random, -reach, reach)); } public class EvaluateTests : TestFixture { public void Ends_AreTheEndCoefficients() { double[] coefficients = { 2, -1, 5, 3 }; CheckEqual(2.0, CubicReferenceDistance.Evaluate(coefficients, 0)); CheckEqual(3.0, CubicReferenceDistance.Evaluate(coefficients, 1)); } public void Interior_MatchesTheFactoredPolynomial() { double[] coefficients = BernsteinFromRoots(0.2, 0.7); CheckGreater(1e-15, Math.Abs(CubicReferenceDistance.Evaluate(coefficients, 0.4) + 0.06)); } } public class SubdivideTests : TestFixture { public void HalvesReproduceThePolynomialOnTheirIntervals() { double[] coefficients = { 2, -1, 5, 3, -4, 1 }; var left = new double[6]; var right = new double[6]; CubicReferenceDistance.Subdivide(coefficients, left, right); for (double s = 0; s <= 1; s += 0.125) { CheckGreater(1e-14, Math.Abs(CubicReferenceDistance.Evaluate(left, s) - CubicReferenceDistance.Evaluate(coefficients, s / 2))); CheckGreater(1e-14, Math.Abs(CubicReferenceDistance.Evaluate(right, s) - CubicReferenceDistance.Evaluate(coefficients, 0.5 + s / 2))); } } public void HalvesShareTheMiddleValue() { double[] coefficients = { 2, -1, 5, 3, -4, 1 }; var left = new double[6]; var right = new double[6]; CubicReferenceDistance.Subdivide(coefficients, left, right); CheckEqual(left[5], right[0]); } } public class SignChangesTests : TestFixture { public void OneSign_IsZero() { CheckEqual(0, CubicReferenceDistance.SignChanges(new double[] { 1, 2, 3 })); } public void Alternating_CountsEachChange() { CheckEqual(3, CubicReferenceDistance.SignChanges(new double[] { 1, -1, 1, -1 })); } public void Zeros_AreSkipped() { CheckEqual(2, CubicReferenceDistance.SignChanges(new double[] { 0, 1, 0, -1, 0, 0, 1, 0 })); } } public class LeadingSignTests : TestFixture { public void SkipsLeadingZeros() { CheckEqual(-1, CubicReferenceDistance.LeadingSign(new double[] { 0, 0, -2, 1 })); } public void AllZero_IsZero() { CheckEqual(0, CubicReferenceDistance.LeadingSign(new double[] { 0, 0, 0 })); } } public class BisectRootTests : TestFixture { public void SimpleRoot_IsFoundToTheLastBit() { double root = CubicReferenceDistance.BisectRoot(BernsteinFromRoots(0.3, -1), low: 0, high: 1, lowSign: -1); CheckGreater(1e-15, Math.Abs(root - 0.3)); } public void RootOnAMidpoint_IsReturnedExactly() { CheckEqual(0.5, CubicReferenceDistance.BisectRoot(new double[] { -1, 1 }, low: 0, high: 1, lowSign: -1)); } public void RootOnTheLowEnd_FindsTheInteriorRoot() { double root = CubicReferenceDistance.BisectRoot(BernsteinFromRoots(0, 0.6), low: 0, high: 1, lowSign: -1); CheckGreater(1e-15, Math.Abs(root - 0.6)); } } public class FootCandidatesTests : TestFixture { public void FiveSeparatedRoots_FindsEachOne() { double[] roots = { 0.1, 0.3, 0.5, 0.7, 0.9 }; var candidates = new List(); CubicReferenceDistance.FootCandidates(BernsteinFromRoots(roots), candidates, CubicReferenceDistance.SmallestInterval, CubicReferenceDistance.IntervalBudget); foreach (double root in roots) { CheckGreater(1e-12, candidates.Min(candidate => Math.Abs(candidate - root))); } } public void NoSignChange_AddsNothing() { var candidates = new List(); CubicReferenceDistance.FootCandidates(new double[] { 1, 2, 1, 3, 1, 2 }, candidates, CubicReferenceDistance.SmallestInterval, CubicReferenceDistance.IntervalBudget); CheckEqual(0, candidates.Count); } public void RootsWithinTheSmallestInterval_ShareOneCandidateAtItsMiddle() { var candidates = new List(); CubicReferenceDistance.FootCandidates(BernsteinFromRoots(0.3, 0.4), candidates, smallestInterval: 0.3, intervalBudget: CubicReferenceDistance.IntervalBudget); CheckEqual(1, candidates.Count); CheckEqual(0.375, candidates[0]); } public void RootExactlyOnASplitPoint_IsAddedOnce() { var candidates = new List(); CubicReferenceDistance.FootCandidates(new double[] { 2, -2, 0, 4 }, candidates, CubicReferenceDistance.SmallestInterval, CubicReferenceDistance.IntervalBudget); CheckEqual(2, candidates.Count); CheckEqual(1, candidates.Count(candidate => candidate == 0.5)); } public void OverBudget_Throws() { CheckThrow(typeof(InvalidOperationException)); CubicReferenceDistance.FootCandidates(BernsteinFromRoots(0.1, 0.3, 0.5, 0.7, 0.9), new List(), CubicReferenceDistance.SmallestInterval, intervalBudget: 1); } } public class FootCoefficientsTests : TestFixture { public void MatchesTheOffsetDottedWithAThirdOfTheTangent() { var pixel = new Vector(1.5, 2); double[] coefficients = CubicReferenceDistance.FootCoefficients(Wave, pixel); for (double t = 0; t <= 1; t += 0.125) { double expected = Vector.DotProduct(Wave.PointAt(t) - pixel, Wave.DerivativeAt(t)) / 3; CheckGreater(1e-12, Math.Abs(CubicReferenceDistance.Evaluate(coefficients, t) - expected)); } } } public class SquaredDistanceAtTests : TestFixture { public void AtTheStart_IsTheSquaredDistanceToTheFirstControlPoint() { CheckEqual(25.0, CubicReferenceDistance.SquaredDistanceAt(Wave, new Vector(3, 4), t: 0)); } public void Interior_IsTheSquaredDistanceToThatCurvePoint() { var pixel = new Vector(1, 2); double expected = Vector.DistanceSquared(Wave.PointAt(0.5), pixel); CheckGreater(1e-14, Math.Abs(expected - CubicReferenceDistance.SquaredDistanceAt(Wave, pixel, t: 0.5))); } } public class SolveTests : TestFixture { [IgnoreTest] public void CheckMatchesBruteForce(CubicPiece piece, Random random, int count, double reach) { for (int i = 0; i < count; i++) { Vector pixel = RandomPoint(random, reach); CheckGreater(1e-9, Math.Abs(CubicReferenceDistance.Solve(piece, pixel).Distance - BruteForceDistance(piece, pixel))); } } public void PointOnTheCurve_IsAtDistanceZero() { CubicReferenceResult result = CubicReferenceDistance.Solve(Wave, Wave.PointAt(0.3)); CheckGreater(1e-15, result.Distance); CheckGreater(1e-12, Math.Abs(result.ClosestT - 0.3)); } public void BehindTheStart_TheStartIsClosest() { CubicReferenceResult result = CubicReferenceDistance.Solve(Wave, new Vector(-2, -1)); CheckEqual(Math.Sqrt(5), result.Distance); CheckEqual(0.0, result.ClosestT); } public void PastTheEnd_TheEndIsClosest() { CubicReferenceResult result = CubicReferenceDistance.Solve(Wave, new Vector(6, 2)); CheckEqual(Math.Sqrt(8), result.Distance); CheckEqual(1.0, result.ClosestT); } public void NearTheCurve_RecoversTheOffset() { const double offset = 1e-9; Vector normal = Wave.DerivativeAt(0.3).NormalizedCopy().PerpCCW(); CubicReferenceResult result = CubicReferenceDistance.Solve(Wave, Wave.PointAt(0.3) + offset * normal); CheckGreater(1e-6, Math.Abs(result.Distance - offset) / offset); } public void DegreeElevatedQuadratic_MatchesTheQuadraticReference() { var random = new Random(7); for (int i = 0; i < 200; i++) { double end = SampleSets.Uniform(random, 0.1, 10); CubicPiece piece = CubicPiece.FromQuadratic(new Vector(0, 0), new Vector(end / 2, 0), new Vector(end, end * end / 2)); var pixel = new Vector(SampleSets.Uniform(random, -5, 5), SampleSets.Uniform(random, -3, 8)); double expected = ReferenceDistance.Solve(ShellSample.FromPosition(pixel.X, pixel.Y, end)).Distance; CheckGreater(1e-12 * (1 + end * end), Math.Abs(CubicReferenceDistance.Solve(piece, pixel).Distance - expected)); } } public void CollinearDoublingBack_TheTurningPointIsClosest() { var piece = new CubicPiece(new Vector(0, 0), new Vector(2, 0), new Vector(-1, 0), new Vector(0, 0)); double turning = (10 - Math.Sqrt(28)) / 18; CubicReferenceResult result = CubicReferenceDistance.Solve(piece, piece.PointAt(turning) + new Vector(1, 0)); CheckGreater(1e-14, Math.Abs(result.Distance - 1)); CheckGreater(1e-7, Math.Abs(result.ClosestT - turning)); } public void AllControlPointsTogether_IsTheDistanceToThatPoint() { var point = new Vector(1, 1); var piece = new CubicPiece(point, point, point, point); CheckEqual(5.0, CubicReferenceDistance.Solve(piece, new Vector(4, 5)).Distance); } public void Cusp_MatchesBruteForce() { CheckMatchesBruteForce(Cusp, new Random(11), count: 200, reach: 1.5); } public void Loop_MatchesBruteForce() { CheckMatchesBruteForce(Loop, new Random(13), count: 200, reach: 2); } public void RandomPieces_MatchBruteForce() { var random = new Random(17); for (int i = 0; i < 50; i++) { var piece = new CubicPiece(RandomPoint(random, 1), RandomPoint(random, 1), RandomPoint(random, 1), RandomPoint(random, 1)); CheckMatchesBruteForce(piece, random, count: 10, reach: 1.5); } } } } }