RoundedPolygon.fromVertices constructor

RoundedPolygon.fromVertices(
  1. List<double> vertices, {
  2. CornerRounding rounding = CornerRounding.unrounded,
  3. List<CornerRounding>? perVertexRounding,
  4. double centerX = double.minPositive,
  5. double centerY = double.minPositive,
})

This function takes the vertices (either supplied or calculated, depending on the constructor called), plus CornerRounding parameters, and creates the actual RoundedPolygon shape, rounding around the vertices (or not) as specified. The result is a list of Cubic curves which represent the geometry of the final shape.

vertices is the list of vertices in this polygon specified as pairs of x/y coordinates in this List<double>. This should be an ordered list (with the outline of the shape going from each vertex to the next in order of this list), otherwise the results will be undefined.

rounding is the CornerRounding properties of all vertices. If some vertices should have different rounding properties, then use perVertexRounding instead. The default rounding value is CornerRounding.unrounded, meaning that the polygon will use the vertices themselves in the final shape and not curves rounded around the vertices.

perVertexRounding is the CornerRounding properties of all vertices. If this parameter is not null, then it must have the same size as vertices. If this parameter is null, then the polygon will use the rounding parameter for every vertex instead. The default value is null.

centerX is the X coordinate of the center of the polygon, around which all vertices will be placed. The default center is at (0,0).

centerY is the Y coordinate of the center of the polygon, around which all vertices will be placed. The default center is at (0,0).

Throws ArgumentError if the number of vertices is less than 3 (the vertices parameter has less than 6 Floats). Or if the perVertexRounding parameter is not null and the size doesn't match the number vertices.

Implementation

// TODO(performance): Update the map calls to more efficient code that
// doesn't allocate Iterators unnecessarily.
factory RoundedPolygon.fromVertices(
  List<double> vertices, {
  CornerRounding rounding = CornerRounding.unrounded,
  List<CornerRounding>? perVertexRounding,
  double centerX = double.minPositive,
  double centerY = double.minPositive,
}) {
  if (vertices.length < 6) {
    throw ArgumentError('Polygons must have at least 3 vertices.');
  }
  if (vertices.length.isOdd) {
    throw ArgumentError('The vertices array should have even size.');
  }
  if (perVertexRounding != null &&
      perVertexRounding.length * 2 != vertices.length) {
    throw ArgumentError('perVertexRounding list should be either null or '
        'the same size as the number of vertices (vertices.size / 2).');
  }
  final corners = <List<Cubic>>[];
  final n = vertices.length ~/ 2;
  final roundedCorners = <_RoundedCorner>[];
  for (var i = 0; i < n; i++) {
    final vtxRounding = perVertexRounding?[i] ?? rounding;
    final prevIndex = ((i + n - 1) % n) * 2;
    final nextIndex = ((i + 1) % n) * 2;
    roundedCorners.add(
      _RoundedCorner(
        Point(vertices[prevIndex], vertices[prevIndex + 1]),
        Point(vertices[i * 2], vertices[i * 2 + 1]),
        Point(vertices[nextIndex], vertices[nextIndex + 1]),
        vtxRounding,
      ),
    );
  }

  // For each side, check if we have enough space to do the cuts needed, and
  // if not split the available space, first for round cuts, then for
  // smoothing if there is space left. Each element in this list is a pair,
  // that represent how much we can do of the cut for the given side (side i
  // goes from corner i to corner i+1), the elements of the pair are: first
  // is how much we can use of expectedRoundCut, second how much of
  // expectedCut.
  final cutAdjusts = List.generate(n, (ix) {
    final expectedRoundCut = roundedCorners[ix].expectedRoundCut +
        roundedCorners[(ix + 1) % n].expectedRoundCut;
    final expectedCut = roundedCorners[ix].expectedCut +
        roundedCorners[(ix + 1) % n].expectedCut;
    final vtxX = vertices[ix * 2];
    final vtxY = vertices[ix * 2 + 1];
    final nextVtxX = vertices[((ix + 1) % n) * 2];
    final nextVtxY = vertices[((ix + 1) % n) * 2 + 1];
    final sideSize = distance(vtxX - nextVtxX, vtxY - nextVtxY);

    // Check expectedRoundCut first, and ensure we fulfill rounding needs
    // first for both corners before using space for smoothing.
    if (expectedRoundCut > sideSize) {
      // Not enough room for fully rounding, see how much we can actually do.
      return (sideSize / expectedRoundCut, 0);
    } else if (expectedCut > sideSize) {
      // We can do full rounding, but not full smoothing.
      return (
        1,
        (sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut)
      );
    } else {
      // There is enough room for rounding & smoothing.
      return (1, 1);
    }
  });

  // Create and store list of beziers for each [potentially] rounded corner.
  for (var i = 0; i < n; i++) {
    // allowedCuts[0] is for the side from the previous corner to this one,
    // allowedCuts[1] is for the side from this corner to the next one.
    final allowedCuts = List<double>.filled(2, 0);

    for (var delta = 0; delta <= 1; delta++) {
      final (roundCutRatio, cutRatio) = cutAdjusts[(i + n - 1 + delta) % n];
      allowedCuts[delta] =
          roundedCorners[i].expectedRoundCut * roundCutRatio +
              (roundedCorners[i].expectedCut -
                      roundedCorners[i].expectedRoundCut) *
                  cutRatio;
    }

    corners.add(
      roundedCorners[i].getCubics(allowedCuts[0], allowedCuts[1]),
    );
  }

  // Finally, store the calculated cubics. This includes all of the rounded
  // corners from above, along with new cubics representing the edges between
  // those corners.
  final tempFeatures = <Feature>[];
  for (var i = 0; i < n; i++) {
    // Note that these indices are for pairs of values (points), they need to
    // be doubled to access the xy values in the vertices float array.
    final prevVtxIndex = (i + n - 1) % n;
    final nextVtxIndex = (i + 1) % n;
    final currVertex = Point(vertices[i * 2], vertices[i * 2 + 1]);
    final prevVertex = Point(
      vertices[prevVtxIndex * 2],
      vertices[prevVtxIndex * 2 + 1],
    );
    final nextVertex = Point(
      vertices[nextVtxIndex * 2],
      vertices[nextVtxIndex * 2 + 1],
    );
    final cvx = convex(prevVertex, currVertex, nextVertex);
    tempFeatures
      ..add(CornerFeature(corners[i], convex: cvx))
      ..add(
        EdgeFeature(
          [
            Cubic.straightLine(
              corners[i].last.anchor1X,
              corners[i].last.anchor1Y,
              corners[(i + 1) % n].first.anchor0X,
              corners[(i + 1) % n].first.anchor0Y,
            ),
          ],
        ),
      );
  }

  final double cX;
  final double cY;

  if (centerX == double.minPositive || centerY == double.minPositive) {
    final center = calculateCenter(vertices);
    cX = center.x;
    cY = center.y;
  } else {
    cX = centerX;
    cY = centerY;
  }

  return RoundedPolygon.fromFeatures(tempFeatures, centerX: cX, centerY: cY);
}