outlineToQuads function
Converts a glyph outline (em space; lines and cubics) into quadratic
curves within tolerance em of the original geometry.
Lines become degenerate quads (control point at the midpoint), so one uniform curve type feeds the band evaluator. Subpaths close implicitly (fill semantics) - the returned set always forms closed loops, which the winding evaluation depends on.
Cubic->quad: the cubic is split into n = ceil(cbrt(err0 / tolerance))
equal-parameter pieces, where err0 = sqrt(3)/36 * |P3 - 3P2 + 3P1 - P0|
is the standard midpoint-parabola error bound for approximating the
whole cubic with a single quadratic; per-piece error then falls as
1/n^3. Each piece's control point is (3Q1 + 3Q2 - Q0 - Q3) / 4.
Implementation
List<QuadCurve> outlineToQuads(PdfPath outline, {double tolerance = 1e-3}) {
final out = <QuadCurve>[];
var penX = 0.0, penY = 0.0, startX = 0.0, startY = 0.0;
var open = false;
void line(double x0, double y0, double x1, double y1) {
if (x0 == x1 && y0 == y1) return;
out.add(QuadCurve(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2, x1, y1));
}
void close() {
if (!open) return;
line(penX, penY, startX, startY);
open = false;
}
for (final segment in outline.segments) {
switch (segment) {
case PdfMoveTo(:final x, :final y):
close();
penX = startX = x;
penY = startY = y;
open = true;
case PdfLineTo(:final x, :final y):
if (!open) continue;
line(penX, penY, x, y);
penX = x;
penY = y;
case PdfCubicTo():
if (!open) continue;
final p0x = penX, p0y = penY;
final p1x = segment.x1, p1y = segment.y1;
final p2x = segment.x2, p2y = segment.y2;
final p3x = segment.x3, p3y = segment.y3;
final dx = p3x - 3 * p2x + 3 * p1x - p0x;
final dy = p3y - 3 * p2y + 3 * p1y - p0y;
final err0 = math.sqrt(3) / 36 * math.sqrt(dx * dx + dy * dy);
final n = err0 <= tolerance
? 1
: math.pow(err0 / tolerance, 1 / 3).ceil().clamp(1, 64);
// piece [t0,t1] endpoints and derivative-based inner Beziers
double bx(double t) {
final mt = 1 - t;
return mt * mt * mt * p0x +
3 * mt * mt * t * p1x +
3 * mt * t * t * p2x +
t * t * t * p3x;
}
double by(double t) {
final mt = 1 - t;
return mt * mt * mt * p0y +
3 * mt * mt * t * p1y +
3 * mt * t * t * p2y +
t * t * t * p3y;
}
double dbx(double t) {
final mt = 1 - t;
return 3 * mt * mt * (p1x - p0x) +
6 * mt * t * (p2x - p1x) +
3 * t * t * (p3x - p2x);
}
double dby(double t) {
final mt = 1 - t;
return 3 * mt * mt * (p1y - p0y) +
6 * mt * t * (p2y - p1y) +
3 * t * t * (p3y - p2y);
}
var qx0 = p0x, qy0 = p0y;
for (var i = 1; i <= n; i++) {
final t0 = (i - 1) / n, t1 = i / n;
final qx3 = i == n ? p3x : bx(t1), qy3 = i == n ? p3y : by(t1);
final dt = (t1 - t0) / 3;
// cubic piece inner controls from endpoint derivatives
final q1x = qx0 + dt * dbx(t0), q1y = qy0 + dt * dby(t0);
final q2x = qx3 - dt * dbx(t1), q2y = qy3 - dt * dby(t1);
// midpoint-parabola control for the piece
final cx = (3 * (q1x + q2x) - qx0 - qx3) / 4;
final cy = (3 * (q1y + q2y) - qy0 - qy3) / 4;
out.add(QuadCurve(qx0, qy0, cx, cy, qx3, qy3));
qx0 = qx3;
qy0 = qy3;
}
penX = p3x;
penY = p3y;
case PdfClosePath():
close();
penX = startX;
penY = startY;
open = true;
}
}
close();
return out;
}