fix: stroke arcs and splines analytically — opaque core, angle-independent weight

LICE_Arc never reaches opacity and ThickFLine's width is minor-axis. One
distance-to-polyline coverage mask, blended once, replaces both.
This commit is contained in:
2026-08-01 13:18:21 -04:00
parent ae9019465e
commit 2e09776342
16 changed files with 1001 additions and 72 deletions
+44
View File
@@ -391,6 +391,48 @@ static void testPixelFromPointMapsCornersAndMidpoint() {
CHECK(clamped.x == 110 && clamped.y == 20);
}
static void testSubpixelMapIsTheIntegerMapBeforeRounding() {
// The antialiased trace draws off subpixelFromPoint while the hit-test still resolves against
// pixelFromPoint. If the two were separate formulas the trace would drift off its own handles,
// so pin the relationship rather than the sub-pixel values: int == round(subpixel), everywhere.
const Box boxes[] = {{10, 20, 100, 51}, {0, 0, 127, 101}, {7, 3, 33, 17}};
for (const Box& box : boxes) {
for (const VelocityCurve& c : {uni(), bip()}) {
for (double v = 0.0; v <= 127.0; v += 1.0) {
for (double a = -1.0; a <= 1.0; a += 0.125) {
const auto ip = c.pixelFromPoint(box, {v, a});
const auto fp = c.subpixelFromPoint(box, {v, a});
CHECK(ip.x == box.left + static_cast<int>(fp.x - box.left + 0.5));
CHECK(ip.y == box.top + static_cast<int>(fp.y - box.top + 0.5));
}
}
}
}
}
static void testSubpixelMapResolvesSlopesTheIntegerMapFlattens() {
// The defect this exists for: adjacent columns of a gentle slope round to the SAME integer
// row, so an integer-only trace is a staircase. The sub-pixel map must separate them.
const Box box{0, 0, 400, 101};
const VelocityCurve c = VelocityCurve::linear();
int identicalIntRows = 0;
double maxSubpixelStep = 0.0, minSubpixelStep = 1e18;
for (int x = 1; x <= 200; ++x) {
const double v0 = c.pointFromPixel(box, x - 1, box.top).velocity;
const double v1 = c.pointFromPixel(box, x, box.top).velocity;
const int y0 = c.pixelFromPoint(box, {v0, c.eval(v0)}).y;
const int y1 = c.pixelFromPoint(box, {v1, c.eval(v1)}).y;
if (y0 == y1) ++identicalIntRows;
const double d = std::fabs(c.subpixelFromPoint(box, {v1, c.eval(v1)}).y -
c.subpixelFromPoint(box, {v0, c.eval(v0)}).y);
if (d > maxSubpixelStep) maxSubpixelStep = d;
if (d < minSubpixelStep) minSubpixelStep = d;
}
CHECK(identicalIntRows > 100); // the integer map really does flatten
CHECK(maxSubpixelStep - minSubpixelStep < 1e-9); // the sub-pixel one advances evenly
CHECK(maxSubpixelStep > 0.0);
}
static void testPointFromPixelInvertsAndClamps() {
const Box box{10, 20, 100, 51};
// Exact corners invert exactly.
@@ -472,6 +514,8 @@ int main() {
testBipolarPixelMapPutsZeroOnTheCentreLine();
testBipolarDragCoversTwiceTheValueRange();
testPixelFromPointMapsCornersAndMidpoint();
testSubpixelMapIsTheIntegerMapBeforeRounding();
testSubpixelMapResolvesSlopesTheIntegerMapFlattens();
testPointFromPixelInvertsAndClamps();
testPixelMapsRoundTripWithinOnePixelQuantum();
testPixelFromPointAgreesWithPointAtPixel();