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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user