strokeToContours function

void strokeToContours(
  1. List<FlatSubpath> subpaths, {
  2. required double width,
  3. required int cap,
  4. required int join,
  5. required double miterLimit,
  6. required StrokeContours out,
})

Expands stroked subpaths (already flattened, already dashed, device pixels) into closed contours appended to out. See the library comment for parameter semantics.

Implementation

void strokeToContours(
  List<FlatSubpath> subpaths, {
  required double width,
  required int cap,
  required int join,
  required double miterLimit,
  required StrokeContours out,
}) {
  final hw = (width <= 0 ? 1.0 : width) / 2;

  void wedge(double px, double py, double ax, double ay, double bx, double by) {
    out._beginRing();
    out._add(px, py);
    out._add(ax, ay);
    out._add(bx, by);
    out._endRing();
  }

  // Circular-arc polygon fanned from (cx, cy); same step density as the GPU
  // triangle fan (<= 0.35 rad per step).
  void fan(double cx, double cy, double fromAngle, double sweep) {
    final steps = math.max(2, (sweep.abs() / 0.35).ceil());
    out._beginRing();
    out._add(cx, cy);
    for (var i = 0; i <= steps; i++) {
      final a = fromAngle + sweep * i / steps;
      out._add(cx + hw * math.cos(a), cy + hw * math.sin(a));
    }
    out._endRing();
  }

  void joinAt(double x, double y, double d0x, double d0y, double d1x,
      double d1y, double n0x, double n0y, double n1x, double n1y) {
    final cross = d0x * d1y - d0y * d1x;
    if (cross.abs() < 1e-9) return; // collinear: quads already meet
    // outer side is the turn's convex side
    final outer0x = cross > 0 ? -n0x : n0x, outer0y = cross > 0 ? -n0y : n0y;
    final outer1x = cross > 0 ? -n1x : n1x, outer1y = cross > 0 ? -n1y : n1y;
    switch (join) {
      case 1: // round
        final a0 = math.atan2(outer0y, outer0x);
        var sweep = math.atan2(outer1y, outer1x) - a0;
        while (sweep > math.pi) {
          sweep -= 2 * math.pi;
        }
        while (sweep < -math.pi) {
          sweep += 2 * math.pi;
        }
        fan(x, y, a0, sweep);
      case 2: // bevel
        wedge(x, y, x + outer0x, y + outer0y, x + outer1x, y + outer1y);
      default: // miter, bevel past the limit
        final mx = outer0x + outer1x, my = outer0y + outer1y;
        final mlen2 = mx * mx + my * my;
        if (mlen2 < 1e-12) {
          wedge(x, y, x + outer0x, y + outer0y, x + outer1x, y + outer1y);
          return;
        }
        // miter point: along (m) scaled so its projection touches both
        // offset lines
        final scale = 2 * hw * hw / mlen2;
        final px = mx * scale, py = my * scale;
        final miterRatio = math.sqrt(px * px + py * py) / hw;
        if (miterRatio > miterLimit) {
          wedge(x, y, x + outer0x, y + outer0y, x + outer1x, y + outer1y);
        } else {
          out._beginRing();
          out._add(x, y);
          out._add(x + outer0x, y + outer0y);
          out._add(x + px, y + py);
          out._add(x + outer1x, y + outer1y);
          out._endRing();
        }
    }
  }

  void squareCap(double x, double y, double dx, double dy) {
    final nx = -dy * hw, ny = dx * hw;
    final ex = x + dx * hw, ey = y + dy * hw;
    out._beginRing();
    out._add(x + nx, y + ny);
    out._add(ex + nx, ey + ny);
    out._add(ex - nx, ey - ny);
    out._add(x - nx, y - ny);
    out._endRing();
  }

  for (final sub in subpaths) {
    // strip consecutive duplicates
    final raw = sub.points;
    final p = DoubleBuilder(raw.length);
    for (var i = 0; i < raw.length; i += 2) {
      final x = raw[i], y = raw[i + 1];
      if (p.length >= 2 &&
          (x - p[p.length - 2]).abs() < 1e-9 &&
          (y - p[p.length - 1]).abs() < 1e-9) {
        continue;
      }
      p.add2(x, y);
    }
    final pts = p.view;
    var n = pts.length ~/ 2;
    final closed = sub.closed && n > 2;
    if (closed && pts[0] == pts[2 * n - 2] && pts[1] == pts[2 * n - 1]) n--;

    if (n == 1) {
      // isolated point: round/square caps paint a dot, butt paints nothing
      if (cap == 1) {
        fan(pts[0], pts[1], 0, math.pi);
        fan(pts[0], pts[1], math.pi, math.pi);
      } else if (cap == 2) {
        out._beginRing();
        out._add(pts[0] - hw, pts[1] - hw);
        out._add(pts[0] + hw, pts[1] - hw);
        out._add(pts[0] + hw, pts[1] + hw);
        out._add(pts[0] - hw, pts[1] + hw);
        out._endRing();
      }
      continue;
    }
    if (n < 2) continue;

    final segs = closed ? n : n - 1;
    var prevNx = 0.0, prevNy = 0.0, prevDirX = 0.0, prevDirY = 0.0;
    for (var s = 0; s < segs; s++) {
      final ax = pts[2 * s], ay = pts[2 * s + 1];
      final bi = (s + 1) % n;
      final bx = pts[2 * bi], by = pts[2 * bi + 1];
      var dx = bx - ax, dy = by - ay;
      final len = math.sqrt(dx * dx + dy * dy);
      if (len < 1e-12) continue;
      dx /= len;
      dy /= len;
      final nx = -dy * hw, ny = dx * hw;

      // segment body quad
      out._beginRing();
      out._add(ax + nx, ay + ny);
      out._add(bx + nx, by + ny);
      out._add(bx - nx, by - ny);
      out._add(ax - nx, ay - ny);
      out._endRing();

      // join with the previous segment at (ax, ay)
      if (s > 0 || closed) {
        if (s == 0 && closed) {
          // the wrap join is emitted after the loop when prev* is known
        } else {
          joinAt(ax, ay, prevDirX, prevDirY, dx, dy, prevNx, prevNy, nx, ny);
        }
      }
      prevNx = nx;
      prevNy = ny;
      prevDirX = dx;
      prevDirY = dy;

      if (closed && s == segs - 1) {
        // wrap join at the start point between the last and first segments
        var fdx = pts[2] - pts[0], fdy = pts[3] - pts[1];
        final flen = math.sqrt(fdx * fdx + fdy * fdy);
        if (flen > 1e-12) {
          fdx /= flen;
          fdy /= flen;
          joinAt(pts[0], pts[1], dx, dy, fdx, fdy, nx, ny, -fdy * hw,
              fdx * hw);
        }
      }
    }

    if (!closed) {
      // caps at both open ends
      var sdx = pts[2] - pts[0], sdy = pts[3] - pts[1];
      final sl = math.sqrt(sdx * sdx + sdy * sdy);
      var edx = pts[2 * n - 2] - pts[2 * n - 4],
          edy = pts[2 * n - 1] - pts[2 * n - 3];
      final el = math.sqrt(edx * edx + edy * edy);
      if (sl > 1e-12 && el > 1e-12) {
        sdx /= sl;
        sdy /= sl;
        edx /= el;
        edy /= el;
        if (cap == 1) {
          final a = math.atan2(sdy, sdx);
          fan(pts[0], pts[1], a + math.pi / 2, math.pi);
          final b = math.atan2(edy, edx);
          fan(pts[2 * n - 2], pts[2 * n - 1], b - math.pi / 2, math.pi);
        } else if (cap == 2) {
          squareCap(pts[0], pts[1], -sdx, -sdy);
          squareCap(pts[2 * n - 2], pts[2 * n - 1], edx, edy);
        }
      }
    }
  }
}