fix: close Θ-W7-T1 review — scaling guard, opacity claims, two vacuous test fixes

Guards the stroke blend against LICE_EXT_GET_SCALING, tightens the analytic-stroker's boxes and NaN handling, corrects the opaque-core threshold and inner-dial rationale in the docs, and re-derives two review-flagged tautological tests so they actually fail against the bugs they claim to catch.
This commit is contained in:
2026-08-01 13:45:43 -04:00
parent 2e09776342
commit 3fb77027c6
11 changed files with 181 additions and 30 deletions
+52 -4
View File
@@ -73,6 +73,8 @@ static float perpendicularWeight(float angleDeg, float halfWidth) {
static void testStraightStrokeHasAnOpaqueCore() {
// The shipped defect stated numerically: peak alpha must reach full, not 137-192/255.
// 2 and 3 px only: below the >= 2 px opaque-core threshold (core/ui/CLAUDE.md), a stroke
// does NOT reliably reach full alpha — see testSubOpaqueCoreAtOnePixelWidth below.
for (float w : {2.0f, 3.0f}) {
for (float deg : {0.0f, 17.0f, 45.0f, 63.0f, 90.0f}) {
const float a = deg * kPi / 180.0f;
@@ -85,6 +87,25 @@ static void testStraightStrokeHasAnOpaqueCore() {
}
}
static void testSubOpaqueCoreAtOnePixelWidth() {
// Below the >= 2 px opaque-core threshold: a 1 px stroke (halfWidth = 0.5) has zero slack
// against the 0.5 px worst-case pixel-centre distance (core/ui/CLAUDE.md), so peak alpha
// tracks the stroke's alignment to the pixel grid instead of reaching 255 everywhere. Pin
// both ends of that modulation — this is the knob track arc's and the mini curve-trace's
// actual behaviour, not a hypothetical.
const StrokePoint onRowCentre[2] = {{20.0f, 100.5f}, {220.0f, 100.5f}}; // centred on row 100
StrokeCanvas aligned;
strokePolyline(aligned, onRowCentre, 2, 0.5f, kBig);
CHECK(aligned.coverageAt(120, 100) >= 0.999f); // aligned to the grid: reaches opaque
const StrokePoint onRowBoundary[2] = {{20.0f, 100.0f}, {220.0f, 100.0f}}; // on the boundary
StrokeCanvas misaligned;
strokePolyline(misaligned, onRowBoundary, 2, 0.5f, kBig);
CHECK(std::fabs(misaligned.coverageAt(120, 99) - 0.5f) < 1e-4f); // split evenly...
CHECK(std::fabs(misaligned.coverageAt(120, 100) - 0.5f) < 1e-4f); // ...across both rows
CHECK(peakCoverage(misaligned) < 0.999f); // and never reaches the opaque core here
}
static void testPerpendicularWeightIsAngleIndependent() {
// The criterion that killed the ThickFLine option: it dips to wid*cos(theta) at every 45
// degrees. An axis-aligned-only sample would pass against it, so sample the diagonals.
@@ -214,12 +235,23 @@ static void testRevisitedRowGapReadsZeroNotGarbage() {
// A circle touches most rows on BOTH sides, leaving an untouched gap between the two spans.
// The row's valid extent grows over that gap, so the gap must be zero-filled, not left at
// whatever the reused scratch buffer held.
StrokeCanvas c;
c.reset(kBig);
c.addSegment(30.0f, 100.0f, 30.0f, 140.0f, 1.5f); // dirty the buffer on the left
c.reset(kBig);
//
// The dirtying pass must land at the SAME bounds/stride the arc pass will reuse, or the two
// writes address disjoint buffer offsets and the "old" value the gap reads back is just the
// scratch buffer's original zero-init — the guard would then have nothing to prove itself
// against (verified: deleting extendRow's fill at stroke_aa.cpp:48-51 left this test passing
// when the dirtying pass used `kBig` while the arc pass reset to its own tighter bounds).
std::vector<StrokePoint> ring;
appendArc(ring, 120.0f, 120.0f, 60.0f, 0.0f, 2.0f * kPi);
const Rect arcBounds = strokeBounds(ring.data(), ring.size(), 1.5f, kBig);
StrokeCanvas c;
c.reset(arcBounds);
// A horizontal segment straight across the row/columns the assertion below checks, so the
// buffer genuinely holds nonzero ink there before the arc's own pass reuses the canvas.
c.addSegment(100.0f, 120.0f, 140.0f, 120.0f, 1.5f);
CHECK(c.coverageAt(120, 120) > 0.9f); // sanity: the dirtying pass actually landed here
strokePolyline(c, ring.data(), ring.size(), 1.5f, kBig);
for (int x = 100; x < 140; ++x) CHECK(c.coverageAt(x, 120) == 0.0f); // hollow middle
}
@@ -257,6 +289,20 @@ static void testCoverageOutsideTheValidSpanReadsZero() {
CHECK(c.coverageAt(1000, 1000) == 0.0f);
}
// --- raster row addressing ----------------------------------------------------
static void testRasterRowOffsetMatchesUnflippedAndFlippedLayouts() {
// Unflipped: row y is just y*rowSpan (LICE's top-down layout).
CHECK(rasterRowOffset(0, 100, 240, false) == 0u);
CHECK(rasterRowOffset(5, 100, 240, false) == 5u * 240u);
CHECK(rasterRowOffset(99, 100, 240, false) == 99u * 240u);
// Flipped (bottom-up DIBs): row y is (height-1-y)*rowSpan — LICE_SysBitmap's own pixel
// accessor, `(h-1-y)*rowspan + x` (lice.cpp:2262).
CHECK(rasterRowOffset(0, 100, 240, true) == 99u * 240u);
CHECK(rasterRowOffset(99, 100, 240, true) == 0u);
CHECK(rasterRowOffset(40, 100, 240, true) == 59u * 240u);
}
// --- long-segment subdivision ------------------------------------------------
static void testSubdivisionDoesNotChangeTheRenderedStroke() {
@@ -381,6 +427,7 @@ static void testKnobArcIsOpaqueAndEvenAllTheWayRound() {
int main() {
testStraightStrokeHasAnOpaqueCore();
testSubOpaqueCoreAtOnePixelWidth();
testPerpendicularWeightIsAngleIndependent();
testWeightHoldsAtTheExactDiagonal();
testZeroLengthSegmentIsARoundDot();
@@ -394,6 +441,7 @@ int main() {
testBoundsClipToTheClipRectAndCoverTheReach();
testStrokeEntirelyOutsideTheClipDrawsNothing();
testCoverageOutsideTheValidSpanReadsZero();
testRasterRowOffsetMatchesUnflippedAndFlippedLayouts();
testSubdivisionDoesNotChangeTheRenderedStroke();
testArcPointsLieOnTheCircleAndRespectTheFlatness();
testArcDensityGrowsWithRadius();