resamplePath function

Float64List resamplePath(
  1. CubicPath path, [
  2. int n = 64,
  3. double? cornerThreshold
])

Samples a cubic subpath at n points equidistant by arc length, anchoring corners and endpoints as exact samples. Returns a Float64List(2n). Closed paths distribute n intervals around the loop (without duplicating the first point); the circular start-point freedom is resolved by the plan's circular correspondence.

Implementation

Float64List resamplePath(
  CubicPath path, [
  int n = 64,
  double? cornerThreshold,
]) {
  final p = path.pts;
  final m = (p.length ~/ 2 - 1) ~/ 3;
  final out = Float64List(2 * n);
  Float64List fill() {
    for (var i = 0; i < n; i++) {
      out[2 * i] = p[0];
      out[2 * i + 1] = p[1];
    }
    return out;
  }

  if (m < 1) return fill();
  final lens = List<double>.filled(m, 0);
  var l = 0.0;
  for (var k = 0; k < m; k++) {
    lens[k] = _segLen(p, k);
    l += lens[k];
  }
  if (l < 1e-12) return fill();

  // Anchors: segment boundaries. For open paths, endpoints + corners. For
  // closed paths, ONLY corners: sampling must be intrinsic to the shape and
  // not to the arbitrary M point — two congruent loops with different start
  // points produce the same sample set (modulo index rotation, which the
  // plan's circular correspondence resolves). With no corners (a circle)
  // the path start is the only possible reference.
  final cs = detectCorners(path, cornerThreshold);
  final List<int> anchors;
  if (path.closed) {
    anchors = cs.isNotEmpty ? cs : <int>[0];
  } else {
    anchors = <int>{0, ...cs, m}.toList()..sort();
  }
  // Runs between anchors; for closed paths the last wraps to anchors[0] + m.
  final runs = <(int, int)>[];
  if (path.closed) {
    for (var j = 0; j < anchors.length; j++) {
      final a = anchors[j];
      final b = j + 1 < anchors.length ? anchors[j + 1] : anchors[0] + m;
      runs.add((a, b));
    }
  } else {
    for (var j = 0; j + 1 < anchors.length; j++) {
      runs.add((anchors[j], anchors[j + 1]));
    }
  }
  final rl = runs.map((r) {
    var s = 0.0;
    for (var k = r.$1; k < r.$2; k++) {
      s += lens[k % m];
    }
    return s;
  }).toList();
  final intervals = path.closed ? n : n - 1;
  if (runs.length > intervals) {
    morphFail('N=$n too small (${runs.length} runs)');
  }

  // Largest-remainder apportionment: proportional to length, min 1, exact sum.
  final sum = rl.fold<double>(0, (a, b) => a + b);
  final total = sum == 0 ? 1.0 : sum;
  final ideal = rl.map((x) => intervals * x / total).toList();
  final counts = ideal.map((q) => math.max(1, q.floor())).toList();
  var r = intervals - counts.fold<int>(0, (a, b) => a + b);
  if (r > 0) {
    // Quantized fraction: the quadrature's fp noise (~1e-15) must not decide
    // the tie-break — runs congruent under rotation must apportion the same
    // in both icons or Procrustes loses the exact congruence. The explicit
    // index tie-break is load-bearing, and doubly so here: Dart's List.sort
    // is not stable, so equal fractions would otherwise resolve arbitrarily.
    final order =
        <(double, int)>[
          for (var idx = 0; idx < ideal.length; idx++)
            (jsRound((ideal[idx] - ideal[idx].floorToDouble()) * 1e9), idx),
        ]..sort((a, b) {
          final c = b.$1.compareTo(a.$1);
          return c != 0 ? c : a.$2.compareTo(b.$2);
        });
    for (var j = 0; j < r; j++) {
      counts[order[j % counts.length].$2]++;
    }
  }
  while (r < 0) {
    var bi = 0;
    for (var idx = 1; idx < counts.length; idx++) {
      if (counts[idx] > counts[bi]) bi = idx;
    }
    if (counts[bi] <= 1) break;
    counts[bi]--;
    r++;
  }

  // Sampling: exact anchor at the start of each run + interiors by inversion.
  var w = 0;
  for (var ri = 0; ri < runs.length; ri++) {
    final (k0, k1) = runs[ri];
    final cnt = counts[ri];
    final lr = rl[ri];
    final vi = 6 * (k0 % m);
    out[2 * w] = p[vi];
    out[2 * w + 1] = p[vi + 1];
    w++;
    var seg = k0;
    var acc = 0.0;
    for (var j = 1; j < cnt; j++) {
      final target = lr * j / cnt;
      while (seg < k1 - 1 && acc + lens[seg % m] < target) {
        acc += lens[seg % m];
        seg++;
      }
      final k = seg % m;
      final ls = lens[k];
      final t = ls > 1e-12 ? _invert(p, k, target - acc, ls) : 0.0;
      _point(p, k, t, out, 2 * w);
      w++;
    }
  }
  if (!path.closed) {
    final vi = 6 * m;
    out[2 * w] = p[vi];
    out[2 * w + 1] = p[vi + 1];
  }
  return out;
}