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:
@@ -81,7 +81,7 @@ test('mid-band: slab shorter side 1000 (between 800 and 1600) → at floor, not
|
||||
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 bitset = new Uint8Array(source.widthPx * source.heightPx).fill(1);
|
||||
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 wExtent = Math.max(...xs) - Math.min(...xs) + 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(hExtent - 576) <= 2, `height extent ${hExtent} ≈ 576 (1800×0.32)`);
|
||||
assert.ok(Math.abs(wExtent - 720) <= 2, `width extent ${wExtent} ≈ 720 (0.9×800)`);
|
||||
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)', () => {
|
||||
|
||||
@@ -136,7 +136,7 @@ test('buildOverlayPlan: length line on axis-right outside slab; widths at 12in s
|
||||
|
||||
// Color contract for draw consumers
|
||||
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)', () => {
|
||||
@@ -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]);
|
||||
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)));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user