Skip to content

Commit

Permalink
Merge pull request #9 from aab29/feature/nearest-methods
Browse files Browse the repository at this point in the history
[FINAL] Adding `nearestTValue` and `indexOfNearestPoint` methods
  • Loading branch information
aab29 authored Jan 31, 2019
2 parents 1894fa7 + affa262 commit 952dc8e
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 1 deletion.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog - bezier

## v 1.1.0 - January 30 2019

- Added `nearestTValue` method to `Bezier` class, based on work by @luigi-rosso -- Thanks!
- Added `indexOfNearestPoint` method (to support `nearestTValue`) in `bezier_tools`
- Added unit tests for both new methods in `bezier_nearest_methods_test`

## v 1.0.3 - December 20 2018

- Corrected the version number in the pubspec, since I forgot to commit it in 1.0.2

## v 1.0.2 - December 20 2018

- Added example directory with a simple HTML demo app
Expand Down
55 changes: 55 additions & 0 deletions lib/src/bezier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -725,4 +725,59 @@ abstract class Bezier {

return lookUpTable;
}

/// Returns the parameter value along the curve that is closest (in terms of
/// geometric distance) to the given [point]. The approximation uses a
/// two-pass projection test that relies on the curve's position look up
/// table. First, the method determines the point in the look up table that
/// is closest to [point]. Afterward, it checks the fine interval around that
/// point to see if a better projection can be found.
///
/// The optional parameter [cachedPositionLookUpTable] allows the method to
/// use previously calculated values for [positionLookUpTable] instead
/// of repeating the calculations. The optional [stepSize] parameter
/// determines how much to increment the parameter value at each iteration
/// when searching the fine interval for the best projection. The default
/// [stepSize] value of 0.1 means that the function will do around twenty
/// iterations. Reducing the value of [stepSize] will increase the number of
/// iterations.
double nearestTValue(Vector2 point,
{List<Vector2> cachedPositionLookUpTable, double stepSize = 0.1}) {
final lookUpTable = cachedPositionLookUpTable ?? positionLookUpTable();

final index = indexOfNearestPoint(lookUpTable, point);

final maxIndex = lookUpTable.length - 1;

if (index == 0) {
return 0.0;
} else if (index == maxIndex) {
return 1.0;
}

final intervalsCount = maxIndex.toDouble();
final t1 = (index - 1) / intervalsCount;
final t2 = (index + 1) / intervalsCount;

final tIncrement = stepSize / intervalsCount;
final maxT = t2 + tIncrement;

var t = t1;
var minSquaredDistance = double.maxFinite;
var nearestT = t1;

while (t < maxT) {
final pointOnCurve = pointAt(t);
final squaredDistance = point.distanceToSquared(pointOnCurve);

if (squaredDistance < minSquaredDistance) {
minSquaredDistance = squaredDistance;
nearestT = t;
}

t += tIncrement;
}

return nearestT;
}
}
19 changes: 19 additions & 0 deletions lib/src/bezier_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,22 @@ List<Intersection> locateIntersectionsRecursively(

return results;
}

/// Returns the index of the point in [points] that is closest (in terms of
/// geometric distance) to [targetPoint].
int indexOfNearestPoint(List<Vector2> points, Vector2 targetPoint) {
var minSquaredDistance = double.maxFinite;
var index;

final pointsCount = points.length;
for (var pointIndex = 0; pointIndex < pointsCount; pointIndex++) {
final point = points[pointIndex];
final squaredDistance = targetPoint.distanceToSquared(point);
if (squaredDistance < minSquaredDistance) {
minSquaredDistance = squaredDistance;
index = pointIndex;
}
}

return index;
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: bezier
version: 1.0.3
version: 1.1.0
authors:
- Aaron Barrett <[email protected]>
- Isaac Barrett <[email protected]>
Expand Down
1 change: 1 addition & 0 deletions test/testing_tools/testing_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export "package:test/test.dart";
export "package:vector_math/vector_math.dart";

export "package:bezier/bezier.dart";
export "package:bezier/src/bezier_tools.dart";

export "matchers/close_to_double.dart";
export "matchers/close_to_vector.dart";
Expand Down
229 changes: 229 additions & 0 deletions test/unit_tests/bezier_nearest_methods_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import "../testing_tools/testing_tools.dart";

void main() {
group("indexOfNearestPoint", () {
test("one point", () {
final points = [new Vector2(80.0, -40.0)];

expect(indexOfNearestPoint(points, new Vector2(-30.0, -30.0)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(-7500.0, 48000.0)),
equals(0));
expect(indexOfNearestPoint(points, new Vector2(0.0, 0.0)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(80.0, -40.0)), equals(0));
});

test("two points", () {
final points = [new Vector2(-1.0, -1.0), new Vector2(1.0, 1.0)];

expect(indexOfNearestPoint(points, new Vector2(-1.0, -1.0)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(1.0, 1.0)), equals(1));
expect(indexOfNearestPoint(points, new Vector2(0.01, 0.01)), equals(1));
expect(indexOfNearestPoint(points, new Vector2(-0.01, -0.01)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(500.0, 500.0)), equals(1));
expect(indexOfNearestPoint(points, new Vector2(-10.0, -10.0)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(-10.0, 2.0)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(7.0, -200.0)), equals(0));
});

test("equidistant solutions prefers earlier element", () {
final points = [new Vector2(-100.0, 0.0), new Vector2(100.0, 0.0)];

expect(indexOfNearestPoint(points, new Vector2(0.0, 0.0)), equals(0));
});

test("distribution a", () {
final points = [
new Vector2(500.0, 10.0),
new Vector2(0.0, 0.0),
new Vector2(-400.0, -20.0),
new Vector2(5.0, 5.0),
new Vector2(150.0, -350.0)
];

expect(indexOfNearestPoint(points, new Vector2(10.0, 10.0)), equals(3));
expect(indexOfNearestPoint(points, new Vector2(500.0, 10.0)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(495.0, 15.0)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(0.0, 0.0)), equals(1));
expect(indexOfNearestPoint(points, new Vector2(-1.0, 0.0)), equals(1));
expect(indexOfNearestPoint(points, new Vector2(1.0, 0.5)), equals(1));
expect(
indexOfNearestPoint(points, new Vector2(-400.0, -20.0)), equals(2));
expect(
indexOfNearestPoint(points, new Vector2(-5000.0, -20.0)), equals(2));
expect(
indexOfNearestPoint(points, new Vector2(-400.0, -17.0)), equals(2));
expect(indexOfNearestPoint(points, new Vector2(5.0, 5.0)), equals(3));
expect(indexOfNearestPoint(points, new Vector2(4.0, 4.0)), equals(3));
expect(indexOfNearestPoint(points, new Vector2(10.0, 10.0)), equals(3));
expect(
indexOfNearestPoint(points, new Vector2(150.0, -350.0)), equals(4));
expect(
indexOfNearestPoint(points, new Vector2(140.0, -340.0)), equals(4));
expect(
indexOfNearestPoint(points, new Vector2(150.0, -9900.0)), equals(4));
expect(indexOfNearestPoint(points, new Vector2(90.0, 250.0)), equals(3));
expect(indexOfNearestPoint(points, new Vector2(0.0, 125.0)), equals(3));
expect(indexOfNearestPoint(points, new Vector2(0.0, -125.0)), equals(1));
expect(indexOfNearestPoint(points, new Vector2(200.0, 0.0)), equals(3));
expect(indexOfNearestPoint(points, new Vector2(-100.0, 0.0)), equals(1));
});

test("from a look-up table", () {
final curve = new CubicBezier([
new Vector2(-100.0, 25.0),
new Vector2(-65.0, -110.0),
new Vector2(0.0, 20.0),
new Vector2(95.0, -100.0)
]);

final points = curve.positionLookUpTable(intervalsCount: 200);

expect(indexOfNearestPoint(points, new Vector2(0.0, 0.0)), equals(122));
expect(
indexOfNearestPoint(points, new Vector2(800.0, 800.0)), equals(180));
expect(
indexOfNearestPoint(points, new Vector2(-1000.0, 1000.0)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(0.0, 100.0)), equals(0));
expect(indexOfNearestPoint(points, new Vector2(-100.0, 0.0)), equals(13));
expect(indexOfNearestPoint(points, new Vector2(100.0, 0.0)), equals(172));
expect(
indexOfNearestPoint(points, new Vector2(-40.0, -10.0)), equals(81));
expect(indexOfNearestPoint(points, new Vector2(-100.0, 25.0)), equals(0));
expect(
indexOfNearestPoint(points, new Vector2(95.0, -100.0)), equals(200));
expect(
indexOfNearestPoint(points, new Vector2(95.0, -190.0)), equals(200));
});
});

group("nearestTValue", () {
test("quadratic", () {
final curve = new QuadraticBezier([
new Vector2(90.0, 0.0),
new Vector2(-10.0, -50.0),
new Vector2(-45.0, 45.0)
]);

expect(curve.nearestTValue(new Vector2(90.0, 0.0)), equals(0.0));
expect(curve.nearestTValue(new Vector2(-45.0, 45.0)), equals(1.0));
expect(
curve.nearestTValue(new Vector2(-10.0, -50.0)), closeToDouble(0.518));
expect(curve.nearestTValue(new Vector2(91.0, 5.0)), equals(0.0));
expect(curve.nearestTValue(new Vector2(-48.0, 48.0)), equals(1.0));
expect(curve.nearestTValue(new Vector2(0.0, 0.0)), closeToDouble(0.586));
expect(
curve.nearestTValue(new Vector2(35.0, -20.0)), closeToDouble(0.306));
expect(
curve.nearestTValue(new Vector2(-45.0, 10.0)), closeToDouble(0.84));
expect(curve.nearestTValue(curve.pointAt(0.034)), closeToDouble(0.034));
expect(curve.nearestTValue(curve.pointAt(0.5)), closeToDouble(0.5));
expect(curve.nearestTValue(curve.pointAt(0.666)), closeToDouble(0.666));
expect(curve.nearestTValue(curve.pointAt(0.75)), closeToDouble(0.75));
expect(curve.nearestTValue(curve.pointAt(0.77)), closeToDouble(0.77));
expect(curve.nearestTValue(curve.pointAt(0.99)), closeToDouble(0.99));

final lookUpTable = curve.positionLookUpTable(intervalsCount: 300);

expect(
curve.nearestTValue(new Vector2(91.0, 5.0),
cachedPositionLookUpTable: lookUpTable),
equals(0.0));
expect(
curve.nearestTValue(new Vector2(-48.0, 48.0),
cachedPositionLookUpTable: lookUpTable),
equals(1.0));
expect(
curve.nearestTValue(new Vector2(0.0, 0.0),
cachedPositionLookUpTable: lookUpTable),
closeToDouble(0.58666666666));
expect(
curve.nearestTValue(new Vector2(24.0, 42.0),
cachedPositionLookUpTable: lookUpTable),
closeToDouble(0.57233333333));

expect(
curve.nearestTValue(new Vector2(0.0, 0.0),
cachedPositionLookUpTable: lookUpTable, stepSize: 0.4),
closeToDouble(0.58733333333));
expect(
curve.nearestTValue(new Vector2(0.0, 0.0),
cachedPositionLookUpTable: lookUpTable, stepSize: 0.01),
closeToDouble(0.5866999999999));
expect(curve.nearestTValue(new Vector2(0.0, 0.0), stepSize: 0.01),
closeToDouble(0.586599999999));
});

test("quadratic, equidistant solutions prefers earlier t value", () {
final curve = new QuadraticBezier([
new Vector2(-1.0, 0.0),
new Vector2(0.0, 100.0),
new Vector2(1.0, 0.0),
]);

expect(curve.nearestTValue(new Vector2(0.0, 0.0)), equals(0.0));
expect(curve.nearestTValue(new Vector2(0.0, 20.0)), closeToDouble(0.112));
});

test("cubic", () {
final curve = new CubicBezier([
new Vector2(-100.0, -100.0),
new Vector2(-80.0, 50.0),
new Vector2(70.0, -50.0),
new Vector2(100.0, 100.0)
]);

expect(curve.nearestTValue(new Vector2(-100.0, -100.0)), equals(0.0));
expect(curve.nearestTValue(new Vector2(100.0, 100.0)), equals(1.0));
expect(
curve.nearestTValue(new Vector2(-80.0, 50.0)), closeToDouble(0.328));
expect(
curve.nearestTValue(new Vector2(70.0, -50.0)), closeToDouble(0.666));
expect(curve.nearestTValue(new Vector2(-110.0, -110.0)), equals(0.0));
expect(curve.nearestTValue(new Vector2(150.0, 190.0)), equals(1.0));
expect(curve.nearestTValue(new Vector2(0.0, 0.0)), closeToDouble(0.514));
expect(
curve.nearestTValue(new Vector2(17.0, -80.0)), closeToDouble(0.492));
expect(
curve.nearestTValue(new Vector2(-55.0, -55.0)), closeToDouble(0.192));
expect(
curve.nearestTValue(new Vector2(25.0, -90.0)), closeToDouble(0.51));
expect(curve.nearestTValue(curve.pointAt(0.01)), closeToDouble(0.01));
expect(curve.nearestTValue(curve.pointAt(0.11)), closeToDouble(0.11));
expect(curve.nearestTValue(curve.pointAt(0.34)), closeToDouble(0.34));
expect(curve.nearestTValue(curve.pointAt(0.5)), closeToDouble(0.5));
expect(curve.nearestTValue(curve.pointAt(0.55)), closeToDouble(0.55));
expect(curve.nearestTValue(curve.pointAt(0.83)), closeToDouble(0.83));
expect(curve.nearestTValue(curve.pointAt(0.99)), closeToDouble(0.99));

final lookUpTable = curve.positionLookUpTable(intervalsCount: 10);

expect(
curve.nearestTValue(new Vector2(-110.0, -110.0),
cachedPositionLookUpTable: lookUpTable),
equals(0.0));
expect(
curve.nearestTValue(new Vector2(150.0, 190.0),
cachedPositionLookUpTable: lookUpTable),
equals(1.0));
expect(
curve.nearestTValue(new Vector2(0.0, 0.0),
cachedPositionLookUpTable: lookUpTable),
closeToDouble(0.51));
expect(
curve.nearestTValue(new Vector2(40.0, -40.0),
cachedPositionLookUpTable: lookUpTable),
closeToDouble(0.6));

expect(
curve.nearestTValue(new Vector2(40.0, -40.0),
cachedPositionLookUpTable: lookUpTable, stepSize: 0.4),
closeToDouble(0.62));
expect(
curve.nearestTValue(new Vector2(40.0, -40.0),
cachedPositionLookUpTable: lookUpTable, stepSize: 0.01),
closeToDouble(0.602));
expect(curve.nearestTValue(new Vector2(40.0, -40.0), stepSize: 0.01),
closeToDouble(0.6024));
});
});
}

0 comments on commit 952dc8e

Please sign in to comment.