validateControlPoints static method

bool validateControlPoints(
  1. List<Offset>? controlPoints,
  2. {double tension = 0.0,
  3. List<String>? reasons}
)

Validates that a given set of control points for a CatmullRomCurve is well-formed and will not produce a spline that self-intersects.

This method is also used in debug mode to validate a curve to make sure that it won't violate the contract for the CatmullRomCurve.new constructor.

If in debug mode, and reasons is non-null, this function will fill in reasons with descriptions of the problems encountered. The reasons argument is ignored in release mode.

In release mode, this function can be used to decide if a proposed modification to the curve will result in a valid curve.

Implementation

static bool validateControlPoints(
  List<Offset>? controlPoints, {
  double tension = 0.0,
  List<String>? reasons,
}) {
  if (controlPoints == null) {
    assert(() {
      reasons?.add('Supplied control points cannot be null');
      return true;
    }());
    return false;
  }

  if (controlPoints.length < 2) {
    assert(() {
      reasons?.add('There must be at least two points supplied to create a valid curve.');
      return true;
    }());
    return false;
  }

  controlPoints = <Offset>[Offset.zero, ...controlPoints, const Offset(1.0, 1.0)];
  final Offset startHandle = controlPoints[0] * 2.0 - controlPoints[1];
  final Offset endHandle = controlPoints.last * 2.0 - controlPoints[controlPoints.length - 2];
  controlPoints = <Offset>[startHandle, ...controlPoints, endHandle];
  double lastX = -double.infinity;
  for (int i = 0; i < controlPoints.length; ++i) {
    if (i > 1 &&
        i < controlPoints.length - 2 &&
        (controlPoints[i].dx <= 0.0 || controlPoints[i].dx >= 1.0)) {
      assert(() {
        reasons?.add(
          'Control points must have X values between 0.0 and 1.0, exclusive. '
          'Point $i has an x value (${controlPoints![i].dx}) which is outside the range.',
        );
        return true;
      }());
      return false;
    }
    if (controlPoints[i].dx <= lastX) {
      assert(() {
        reasons?.add(
          'Each X coordinate must be greater than the preceding X coordinate '
          '(i.e. must be monotonically increasing in X). Point $i has an x value of '
          '${controlPoints![i].dx}, which is not greater than $lastX',
        );
        return true;
      }());
      return false;
    }
    lastX = controlPoints[i].dx;
  }

  bool success = true;

  // An empirical test to make sure things are single-valued in X.
  lastX = -double.infinity;
  const double tolerance = 1e-3;
  final CatmullRomSpline testSpline = CatmullRomSpline(controlPoints, tension: tension);
  final double start = testSpline.findInverse(0.0);
  final double end = testSpline.findInverse(1.0);
  final Iterable<Curve2DSample> samplePoints = testSpline.generateSamples(start: start, end: end);
  /// If the first and last points in the samples aren't at (0,0) or (1,1)
  /// respectively, then the curve is multi-valued at the ends.
  if (samplePoints.first.value.dy.abs() > tolerance || (1.0 - samplePoints.last.value.dy).abs() > tolerance) {
    bool bail = true;
    success = false;
    assert(() {
      reasons?.add(
        'The curve has more than one Y value at X = ${samplePoints.first.value.dx}. '
        'Try moving some control points further away from this value of X, or increasing '
        'the tension.',
      );
      // No need to keep going if we're not giving reasons.
      bail = reasons == null;
      return true;
    }());
    if (bail) {
      // If we're not in debug mode, then we want to bail immediately
      // instead of checking everything else.
      return false;
    }
  }
  for (final Curve2DSample sample in samplePoints) {
    final Offset point = sample.value;
    final double t = sample.t;
    final double x = point.dx;
    if (t >= start && t <= end && (x < -1e-3 || x > 1.0 + 1e-3)) {
      bool bail = true;
      success = false;
      assert(() {
        reasons?.add(
          'The resulting curve has an X value ($x) which is outside '
          'the range [0.0, 1.0], inclusive.',
        );
        // No need to keep going if we're not giving reasons.
        bail = reasons == null;
        return true;
      }());
      if (bail) {
        // If we're not in debug mode, then we want to bail immediately
        // instead of checking all the segments.
        return false;
      }
    }
    if (x < lastX) {
      bool bail = true;
      success = false;
      assert(() {
        reasons?.add(
          'The curve has more than one Y value at x = $x. Try moving '
          'some control points further apart in X, or increasing the tension.',
        );
        // No need to keep going if we're not giving reasons.
        bail = reasons == null;
        return true;
      }());
      if (bail) {
        // If we're not in debug mode, then we want to bail immediately
        // instead of checking all the segments.
        return false;
      }
    }
    lastX = x;
  }
  return success;
}