test(photos): overlay UAT nits + 90% fill expect (#67,#68)

Lock length dim to mask tip-to-tip (not oversized bbox), require width
labelAt on axis-left off the wood, smaller font contract, and 90% crop
fill extent. Red before green.
This commit is contained in:
2026-09-15 21:46:27 +00:00
parent 5650255e49
commit 15895cae0c
2 changed files with 111 additions and 4 deletions
+3 -3
View File
@@ -81,7 +81,7 @@ test('mid-band: slab shorter side 1000 (between 800 and 1600) → at floor, not
assert.equal(out.width, 800); assert.equal(out.width, 800);
}); });
test('80% fill: limiting dimension maps to 0.8 × frame side', () => { test('90% fill: limiting dimension maps to 0.9 × frame side (#68)', () => {
const source: Pick<Source, 'widthPx' | 'heightPx'> = { widthPx: 2200, heightPx: 2000 }; const source: Pick<Source, 'widthPx' | 'heightPx'> = { widthPx: 2200, heightPx: 2000 };
const bitset = new Uint8Array(source.widthPx * source.heightPx).fill(1); const bitset = new Uint8Array(source.widthPx * source.heightPx).fill(1);
const out = composeInventoryFrame( const out = composeInventoryFrame(
@@ -103,8 +103,8 @@ test('80% fill: limiting dimension maps to 0.8 × frame side', () => {
const xs = [minx, maxx], ys = [miny, maxy]; const xs = [minx, maxx], ys = [miny, maxy];
const wExtent = Math.max(...xs) - Math.min(...xs) + 1; const wExtent = Math.max(...xs) - Math.min(...xs) + 1;
const hExtent = Math.max(...ys) - Math.min(...ys) + 1; const hExtent = Math.max(...ys) - Math.min(...ys) + 1;
assert.ok(Math.abs(wExtent - 640) <= 2, `width extent ${wExtent} ≈ 640 (0.8×800)`); assert.ok(Math.abs(wExtent - 720) <= 2, `width extent ${wExtent} ≈ 720 (0.9×800)`);
assert.ok(Math.abs(hExtent - 576) <= 2, `height extent ${hExtent} ≈ 576 (1800×0.32)`); assert.ok(Math.abs(hExtent - 648) <= 2, `height extent ${hExtent} ≈ 648 (1800×0.36)`);
}); });
test('belowFloor: small slab → sourceBelowFloor, outW capped at floor (no upscale)', () => { test('belowFloor: small slab → sourceBelowFloor, outW capped at floor (no upscale)', () => {
+108 -1
View File
@@ -136,7 +136,7 @@ test('buildOverlayPlan: length line on axis-right outside slab; widths at 12in s
// Color contract for draw consumers // Color contract for draw consumers
assert.equal(plan.stroke, '#000000'); assert.equal(plan.stroke, '#000000');
assert.ok(plan.fontPx >= 24, `fontPx ${plan.fontPx} large enough to read`); assert.ok(plan.fontPx >= 18, `fontPx ${plan.fontPx} large enough to read`);
}); });
test('buildOverlayPlan: stations measured from EXIF bottom, not tip-inclusive M1 every-6 (#63)', () => { test('buildOverlayPlan: stations measured from EXIF bottom, not tip-inclusive M1 every-6 (#63)', () => {
@@ -185,3 +185,110 @@ test('buildOverlayPlan: dishonest upright-photo axes rejected — uses approved
const ly = Math.abs(plan.length.a[1] - plan.length.b[1]); const ly = Math.abs(plan.length.a[1] - plan.length.b[1]);
assert.ok(lx > ly * 2, `length graphic follows approved +x axis (dx=${lx}, dy=${ly})`); assert.ok(lx > ly * 2, `length graphic follows approved +x axis (dx=${lx}, dy=${ly})`);
}); });
// --- Ty UAT nits (#67) ---
test('buildOverlayPlan: length endpoints snap to mask tips, not oversized bbox corners (#67)', () => {
// Wood occupies x=100..299 only; synthetic bbox long side extends past tips (x=50..350).
const W = 400, H = 160;
const bitset = new Uint8Array(W * H);
for (let y = 40; y < 100; y++) for (let x = 100; x < 300; x++) bitset[y * W + x] = 1;
const bbox = {
corners: [
[350, 100],
[350, 40],
[50, 40],
[50, 100],
] as [number, number][],
longSidePx: 300,
angleDeg: 0,
};
const plan = buildOverlayPlan({
bitset,
widthPx: W,
heightPx: H,
bbox,
scale: 10,
lengthIn: 20,
lengthLabel: '20"',
mapToInventory: (x, y) => [x, y],
});
// Length line sits on axis-right; project a/b onto the length axis through the
// wood centroid — endpoints must match mask span ends, not bbox corners.
const oriented = pickBottomTowardTop([100, 70], [299, 70]);
const strip = (p: [number, number]): [number, number] => {
const cx = 199.5, cy = 70;
const along = (p[0] - cx) * oriented.ux + (p[1] - cy) * oriented.uy;
return [cx + oriented.ux * along, cy + oriented.uy * along];
};
const xs = [strip(plan.length.a)[0], strip(plan.length.b)[0]].sort((a, b) => a - b);
// Mask tips ≈ 100 and 299 (inclusive pixels); bbox corners at 50 and 350
assert.ok(Math.abs(xs[0] - 100) <= 2, `mask min tip ${xs[0]} ≈ 100, not bbox 50`);
assert.ok(Math.abs(xs[1] - 299) <= 2, `mask max tip ${xs[1]} ≈ 299, not bbox 350`);
assert.ok(Math.abs(xs[0] - 50) > 20, 'must not use bbox corner at 50');
assert.ok(Math.abs(xs[1] - 350) > 20, 'must not use bbox corner at 350');
});
test('buildOverlayPlan: width labels sit axis-left outside the slab (#67)', () => {
// Uniform strip: length +x, width along y. Axis-right of bottom→top is −y (up).
const W = 480, H = 200;
const bitset = new Uint8Array(W * H);
for (let y = 50; y < 150; y++) for (let x = 0; x < W; x++) bitset[y * W + x] = 1;
const box = orientedRect(minAreaRect(bitset, W, H)!, 0);
const plan = buildOverlayPlan({
bitset,
widthPx: W,
heightPx: H,
bbox: box,
scale: 10,
lengthIn: 48,
lengthLabel: '48"',
mapToInventory: (x, y) => [x, y],
});
assert.ok(plan.widths.length > 0);
for (const w of plan.widths) {
assert.ok(w.labelAt, 'width mark must expose labelAt');
// left/right tips still on wood edges
assert.ok(Math.abs(w.left[1] - 149) <= 2 || Math.abs(w.left[1] - 50) <= 2);
assert.ok(Math.abs(w.right[1] - 149) <= 2 || Math.abs(w.right[1] - 50) <= 2);
// labelAt on axis-left = opposite of axis-right from center
// For this geometry, axis-right is toward smaller y; axis-left toward larger y
// (outside past the bottom edge y≈149).
const cy = w.center[1];
assert.ok(
w.labelAt[1] > Math.max(w.left[1], w.right[1]) + 4,
`width labelAt y=${w.labelAt[1]} must sit past slab edge (left of travel), not on wood center y=${cy}`,
);
assert.ok(
Math.abs(w.labelAt[0] - w.center[0]) < 3,
'width label stays on the station cross-section (same x as center)',
);
}
// Length stays axis-right (outside opposite side)
const lenY = (plan.length.a[1] + plan.length.b[1]) / 2;
assert.ok(lenY < 50 - 4, `length dim stays axis-right outside top edge, got y=${lenY}`);
});
test('buildOverlayPlan: font one step smaller than prior 28 / scale*1.2 (#67)', () => {
const W = 480, H = 200;
const bitset = new Uint8Array(W * H);
for (let y = 50; y < 150; y++) for (let x = 0; x < W; x++) bitset[y * W + x] = 1;
const box = orientedRect(minAreaRect(bitset, W, H)!, 0);
const scale = 10;
const plan = buildOverlayPlan({
bitset,
widthPx: W,
heightPx: H,
bbox: box,
scale,
lengthIn: 48,
lengthLabel: '48"',
mapToInventory: (x, y) => [x, y],
});
const oldFont = Math.max(28, Math.round(scale * 1.2));
assert.ok(plan.fontPx < oldFont, `fontPx ${plan.fontPx} must be < prior ${oldFont}`);
assert.ok(plan.fontPx >= 18, `fontPx ${plan.fontPx} still legible`);
// Concrete contract: max(22, round(scale*1.0))
assert.equal(plan.fontPx, Math.max(22, Math.round(scale * 1.0)));
});