void arcTo()

in lib/web_ui/lib/src/engine/html/path/path.dart [436:642]


  void arcTo(
      ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) {
    assert(rectIsValid(rect));
    // If width or height is 0, we still stroke a line, only abort if both
    // are empty.
    if (rect.width == 0 && rect.height == 0) {
      return;
    }
    if (pathRef.countPoints() == 0) {
      forceMoveTo = true;
    }
    final ui.Offset? lonePoint = _arcIsLonePoint(rect, startAngle, sweepAngle);
    if (lonePoint != null) {
      if (forceMoveTo) {
        moveTo(lonePoint.dx, lonePoint.dy);
      } else {
        lineTo(lonePoint.dx, lonePoint.dy);
      }
    }
    // Convert angles to unit vectors.
    double stopAngle = startAngle + sweepAngle;
    final double cosStart = math.cos(startAngle);
    final double sinStart = math.sin(startAngle);
    double cosStop = math.cos(stopAngle);
    double sinStop = math.sin(stopAngle);

    // If the sweep angle is nearly (but less than) 360, then due to precision
    // loss in radians-conversion and/or sin/cos, we may end up with coincident
    // vectors, which will fool quad arc build into doing nothing (bad) instead
    // of drawing a nearly complete circle (good).
    // e.g. canvas.drawArc(0, 359.99, ...)
    // -vs- canvas.drawArc(0, 359.9, ...)
    // Detect this edge case, and tweak the stop vector.
    if (SPath.nearlyEqual(cosStart, cosStop) && SPath.nearlyEqual(sinStart, sinStop)) {
      final double sweep = sweepAngle.abs() * 180.0 / math.pi;
      if (sweep <= 360 && sweep > 359) {
        // Use tiny angle (in radians) to tweak.
        final double deltaRad = sweepAngle < 0 ? -1.0 / 512.0 : 1.0 / 512.0;
        do {
          stopAngle -= deltaRad;
          cosStop = math.cos(stopAngle);
          sinStop = math.sin(stopAngle);
        } while (cosStart == cosStop && sinStart == sinStop);
      }
    }
    final int dir = sweepAngle > 0 ? SPathDirection.kCW : SPathDirection.kCCW;
    final double endAngle = startAngle + sweepAngle;
    final double radiusX = rect.width / 2.0;
    final double radiusY = rect.height / 2.0;
    final double px = rect.center.dx + (radiusX * math.cos(endAngle));
    final double py = rect.center.dy + (radiusY * math.sin(endAngle));
    // At this point, we know that the arc is not a lone point, but
    // startV == stopV indicates that the sweepAngle is too small such that
    // angles_to_unit_vectors cannot handle it
    if (cosStart == cosStop && sinStart == sinStop) {
      // Add moveTo to start point if forceMoveTo is true. Otherwise a lineTo
      // unless we're sufficiently close to start point currently. This prevents
      // spurious lineTos when adding a series of contiguous arcs from the same
      // oval.
      if (forceMoveTo) {
        moveTo(px, py);
      } else {
        _lineToIfNotTooCloseToLastPoint(px, py);
      }
      // We are done with tiny sweep approximated by line.
      return;
    }

    // Convert arc defined by start/end unit vectors to conics (max 5).

    // Dot product
    final double x = (cosStart * cosStop) + (sinStart * sinStop);
    // Cross product
    double y = (cosStart * sinStop) - (sinStart * cosStop);
    final double absY = y.abs();
    // Check for coincident vectors (angle is nearly 0 or 180).
    // The cross product for angles 0 and 180 will be zero, we use the
    // dot product sign to distinguish between the two.
    if (absY <= SPath.scalarNearlyZero &&
        x > 0 &&
        ((y >= 0 && dir == SPathDirection.kCW) ||
            (y <= 0 && dir == SPathDirection.kCCW))) {
      // No conics, just use single line to connect point.
      if (forceMoveTo) {
        moveTo(px, py);
      } else {
        _lineToIfNotTooCloseToLastPoint(px, py);
      }
      return;
    }

    // Normalize to clockwise
    if (dir == SPathDirection.kCCW) {
      y = -y;
    }

    // Use 1 conic per quadrant of a circle.
    // 0..90 -> quadrant 0
    // 90..180 -> quadrant 1
    // 180..270 -> quadrant 2
    // 270..360 -> quadrant 3

    const List<ui.Offset> quadPoints = <ui.Offset>[
      ui.Offset(1, 0),
      ui.Offset(1, 1),
      ui.Offset(0, 1),
      ui.Offset(-1, 1),
      ui.Offset(-1, 0),
      ui.Offset(-1, -1),
      ui.Offset(0, -1),
      ui.Offset(1, -1),
    ];

    int quadrant = 0;
    if (0 == y) {
      // 180 degrees between vectors.
      quadrant = 2;
      assert((x + 1).abs() <= SPath.scalarNearlyZero);
    } else if (0 == x) {
      // Dot product 0 means 90 degrees between vectors.
      assert((absY - 1) <= SPath.scalarNearlyZero);
      quadrant = y > 0 ? 1 : 3; // 90 or 270
    } else {
      if (y < 0) {
        quadrant += 2;
      }
      if ((x < 0) != (y < 0)) {
        quadrant += 1;
      }
    }

    final List<Conic> conics = <Conic>[];

    const double quadrantWeight = SPath.scalarRoot2Over2;
    int conicCount = quadrant;
    for (int i = 0; i < conicCount; i++) {
      final int quadPointIndex = i * 2;
      final ui.Offset p0 = quadPoints[quadPointIndex];
      final ui.Offset p1 = quadPoints[quadPointIndex + 1];
      final ui.Offset p2 = quadPoints[quadPointIndex + 2];
      conics
          .add(Conic(p0.dx, p0.dy, p1.dx, p1.dy, p2.dx, p2.dy, quadrantWeight));
    }

    // Now compute any remaining ( < 90degree ) arc for last conic.
    final double finalPx = x;
    final double finalPy = y;
    final ui.Offset lastQuadrantPoint = quadPoints[quadrant * 2];
    // Dot product between last quadrant vector and last point on arc.
    final double dot = (x * lastQuadrantPoint.dx) + (y * lastQuadrantPoint.dy);
    if (dot < 1) {
      // Compute the bisector vector and then rescale to be the off curve point.
      // Length is cos(theta/2) using half angle identity we get
      // length = sqrt(2 / (1 + cos(theta)). We already have cos from computing
      // dot. Computed weight is cos(theta/2).
      double offCurveX = lastQuadrantPoint.dx + x;
      double offCurveY = lastQuadrantPoint.dy + y;
      final double cosThetaOver2 = math.sqrt((1.0 + dot) / 2.0);
      final double unscaledLength =
          math.sqrt((offCurveX * offCurveX) + (offCurveY * offCurveY));
      assert(unscaledLength > SPath.scalarNearlyZero);
      offCurveX /= cosThetaOver2 * unscaledLength;
      offCurveY /= cosThetaOver2 * unscaledLength;
      if (!SPath.nearlyEqual(offCurveX, lastQuadrantPoint.dx) ||
          !SPath.nearlyEqual(offCurveY, lastQuadrantPoint.dy)) {
        conics.add(Conic(lastQuadrantPoint.dx, lastQuadrantPoint.dy, offCurveX,
            offCurveY, finalPx, finalPy, cosThetaOver2));
        ++conicCount;
      }
    }

    // Any points we generate based on unit vectors cos/sinStart , cos/sinStop
    // we rotate to start vector, scale by rect.width/2 rect.height/2 and
    // then translate to center point.
    final double scaleX = rect.width / 2;
    final bool ccw = dir == SPathDirection.kCCW;
    final double scaleY = rect.height / 2;
    final double centerX = rect.center.dx;
    final double centerY = rect.center.dy;
    for (final Conic conic in conics) {
      double x = conic.p0x;
      double y = ccw ? -conic.p0y : conic.p0y;
      conic.p0x = (cosStart * x - sinStart * y) * scaleX + centerX;
      conic.p0y = (cosStart * y + sinStart * x) * scaleY + centerY;
      x = conic.p1x;
      y = ccw ? -conic.p1y : conic.p1y;
      conic.p1x = (cosStart * x - sinStart * y) * scaleX + centerX;
      conic.p1y = (cosStart * y + sinStart * x) * scaleY + centerY;
      x = conic.p2x;
      y = ccw ? -conic.p2y : conic.p2y;
      conic.p2x = (cosStart * x - sinStart * y) * scaleX + centerX;
      conic.p2y = (cosStart * y + sinStart * x) * scaleY + centerY;
    }
    // Now output points.
    final double firstConicPx = conics[0].p0x;
    final double firstConicPy = conics[0].p0y;
    if (forceMoveTo) {
      moveTo(firstConicPx, firstConicPy);
    } else {
      _lineToIfNotTooCloseToLastPoint(firstConicPx, firstConicPy);
    }
    for (int i = 0; i < conicCount; i++) {
      final Conic conic = conics[i];
      conicTo(conic.p1x, conic.p1y, conic.p2x, conic.p2y, conic.fW);
    }
    _resetAfterEdit();
  }