test(photos): length dim snaps to wood tips (#69)
This commit is contained in:
@@ -5,9 +5,11 @@ import {
|
||||
pickBottomTowardTop,
|
||||
axisRightUnit,
|
||||
buildOverlayPlan,
|
||||
inventorySourceMap,
|
||||
type OverlayPlan,
|
||||
} from './overlay.ts';
|
||||
import { orientedRect, minAreaRect } from '../geometry/rect.ts';
|
||||
import { axisEndpoints } from '../measure/measure.ts';
|
||||
|
||||
// --- Station math (#63) ---
|
||||
|
||||
@@ -292,3 +294,186 @@ test('buildOverlayPlan: font one step smaller than prior 28 / scale*1.2 (#67)',
|
||||
// Concrete contract: max(22, round(scale*1.0))
|
||||
assert.equal(plan.fontPx, Math.max(22, Math.round(scale * 1.0)));
|
||||
});
|
||||
|
||||
|
||||
// --- Length dim must not overshoot wood footprint (#69) ---
|
||||
|
||||
/** Tilted-axis slab: irregular mask + approved bbox long side ~10° from vertical. */
|
||||
function tiltedSlabFixture() {
|
||||
const W = 300, H = 600;
|
||||
const bitset = new Uint8Array(W * H);
|
||||
for (let y = 50; y <= 550; y++) {
|
||||
const t = (y - 50) / 500;
|
||||
const x0 = Math.round(80 + t * 20);
|
||||
const x1 = Math.round(180 + t * 40);
|
||||
for (let x = x0; x <= x1; x++) bitset[y * W + x] = 1;
|
||||
}
|
||||
const ang = (10 * Math.PI) / 180;
|
||||
const ux0 = Math.sin(ang);
|
||||
const uy0 = Math.cos(ang);
|
||||
const rx0 = -uy0;
|
||||
const ry0 = ux0;
|
||||
const cx = 140, cy = 300, halfL = 280, halfW = 90;
|
||||
const corners: [number, number][] = [
|
||||
[cx + ux0 * halfL + rx0 * halfW, cy + uy0 * halfL + ry0 * halfW],
|
||||
[cx + ux0 * halfL - rx0 * halfW, cy + uy0 * halfL - ry0 * halfW],
|
||||
[cx - ux0 * halfL - rx0 * halfW, cy - uy0 * halfL - ry0 * halfW],
|
||||
[cx - ux0 * halfL + rx0 * halfW, cy - uy0 * halfL + ry0 * halfW],
|
||||
];
|
||||
const bbox = { corners, longSidePx: halfL * 2, angleDeg: 80 };
|
||||
return { W, H, bitset, bbox };
|
||||
}
|
||||
|
||||
function maskWoodAabb(bitset: Uint8Array, W: number, H: number) {
|
||||
let minX = W, maxX = 0, minY = H, maxY = 0;
|
||||
for (let y = 0; y < H; y++) {
|
||||
for (let x = 0; x < W; x++) {
|
||||
if (!bitset[y * W + x]) continue;
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
return { minX, maxX, minY, maxY };
|
||||
}
|
||||
|
||||
test('buildOverlayPlan: length axial span = tip span; inventory y stays within wood AABB (#69)', () => {
|
||||
const { W, H, bitset, bbox } = tiltedSlabFixture();
|
||||
const scale = 10;
|
||||
const wood = maskWoodAabb(bitset, W, H);
|
||||
const slabBounds = {
|
||||
x: wood.minX - 8,
|
||||
y: wood.minY - 8,
|
||||
w: wood.maxX - wood.minX + 16,
|
||||
h: wood.maxY - wood.minY + 16,
|
||||
};
|
||||
const inv = inventorySourceMap(slabBounds, { widthPx: W, heightPx: H }, 900, 1200);
|
||||
const plan = buildOverlayPlan({
|
||||
bitset,
|
||||
widthPx: W,
|
||||
heightPx: H,
|
||||
bbox,
|
||||
scale,
|
||||
lengthIn: 50,
|
||||
lengthLabel: '50"',
|
||||
mapToInventory: inv.mapToInventory,
|
||||
});
|
||||
|
||||
// Wood AABB in inventory space
|
||||
let mMinY = Infinity, mMaxY = -Infinity;
|
||||
let mMinX = Infinity, mMaxX = -Infinity;
|
||||
for (let y = 0; y < H; y++) {
|
||||
for (let x = 0; x < W; x++) {
|
||||
if (!bitset[y * W + x]) continue;
|
||||
const [mx, my] = inv.mapToInventory(x, y);
|
||||
if (my < mMinY) mMinY = my;
|
||||
if (my > mMaxY) mMaxY = my;
|
||||
if (mx < mMinX) mMinX = mx;
|
||||
if (mx > mMaxX) mMaxX = mx;
|
||||
}
|
||||
}
|
||||
const lenMinY = Math.min(plan.length.a[1], plan.length.b[1]);
|
||||
const lenMaxY = Math.max(plan.length.a[1], plan.length.b[1]);
|
||||
// Current bug: (rx,ry)*outward pushes length ends past wood tip y when axis is tilted
|
||||
assert.ok(
|
||||
lenMinY >= mMinY - 1.5,
|
||||
`length top y=${lenMinY.toFixed(1)} must not extend past wood AABB minY=${mMinY.toFixed(1)}`,
|
||||
);
|
||||
assert.ok(
|
||||
lenMaxY <= mMaxY + 1.5,
|
||||
`length bottom y=${lenMaxY.toFixed(1)} must not extend past wood AABB maxY=${mMaxY.toFixed(1)}`,
|
||||
);
|
||||
|
||||
// Measured axial span equals mask tip-to-tip (dim line parallel beside wood)
|
||||
const { start: aa, end: bb } = axisEndpoints(bbox);
|
||||
const ex = bb[0] - aa[0], ey = bb[1] - aa[1], elen = Math.hypot(ex, ey) || 1;
|
||||
const ux = ex / elen, uy = ey / elen;
|
||||
const cx = bbox.corners.reduce((s, c) => s + c[0], 0) / 4;
|
||||
const cy = bbox.corners.reduce((s, c) => s + c[1], 0) / 4;
|
||||
const cProj = cx * ux + cy * uy;
|
||||
let tipMin = Infinity, tipMax = -Infinity;
|
||||
for (let y = 0; y < H; y++) {
|
||||
for (let x = 0; x < W; x++) {
|
||||
if (!bitset[y * W + x]) continue;
|
||||
const p = x * ux + y * uy;
|
||||
if (p < tipMin) tipMin = p;
|
||||
if (p > tipMax) tipMax = p;
|
||||
}
|
||||
}
|
||||
const tipStart: [number, number] = [cx + ux * (tipMin - cProj), cy + uy * (tipMin - cProj)];
|
||||
const tipEnd: [number, number] = [cx + ux * (tipMax - cProj), cy + uy * (tipMax - cProj)];
|
||||
const oriented = pickBottomTowardTop(tipStart, tipEnd);
|
||||
const mapBottom = inv.mapToInventory(oriented.bottom[0], oriented.bottom[1]);
|
||||
const mapTop = inv.mapToInventory(oriented.top[0], oriented.top[1]);
|
||||
const proj = (p: [number, number], o: [number, number]) =>
|
||||
(p[0] - o[0]) * oriented.ux + (p[1] - o[1]) * oriented.uy;
|
||||
const tipSpan = Math.abs(proj(mapTop, mapBottom));
|
||||
const lenSpan = Math.abs(proj(plan.length.b, plan.length.a));
|
||||
assert.ok(
|
||||
Math.abs(lenSpan - tipSpan) <= 2,
|
||||
`length axial span ${lenSpan.toFixed(1)} must equal tip span ${tipSpan.toFixed(1)}`,
|
||||
);
|
||||
|
||||
// Length stays axis-right of wood (clearance to the right in inventory x for this fixture)
|
||||
const lenMidX = (plan.length.a[0] + plan.length.b[0]) / 2;
|
||||
assert.ok(lenMidX > mMaxX + 4, `length dim must sit outside wood on axis-right (x=${lenMidX} vs maxX=${mMaxX})`);
|
||||
});
|
||||
|
||||
test('buildOverlayPlan: width labels fully left of wood AABB after inventory map (#69)', () => {
|
||||
const { W, H, bitset, bbox } = tiltedSlabFixture();
|
||||
const scale = 10;
|
||||
const wood = maskWoodAabb(bitset, W, H);
|
||||
const slabBounds = {
|
||||
x: wood.minX - 8,
|
||||
y: wood.minY - 8,
|
||||
w: wood.maxX - wood.minX + 16,
|
||||
h: wood.maxY - wood.minY + 16,
|
||||
};
|
||||
const inv = inventorySourceMap(slabBounds, { widthPx: W, heightPx: H }, 900, 1200);
|
||||
const plan = buildOverlayPlan({
|
||||
bitset,
|
||||
widthPx: W,
|
||||
heightPx: H,
|
||||
bbox,
|
||||
scale,
|
||||
lengthIn: 50,
|
||||
lengthLabel: '50"',
|
||||
mapToInventory: inv.mapToInventory,
|
||||
});
|
||||
assert.ok(plan.widths.length > 0, 'expected width stations');
|
||||
|
||||
let mMinX = Infinity, mMaxX = -Infinity, mMinY = Infinity, mMaxY = -Infinity;
|
||||
for (let y = 0; y < H; y++) {
|
||||
for (let x = 0; x < W; x++) {
|
||||
if (!bitset[y * W + x]) continue;
|
||||
const [mx, my] = inv.mapToInventory(x, y);
|
||||
if (mx < mMinX) mMinX = mx;
|
||||
if (mx > mMaxX) mMaxX = mx;
|
||||
if (my < mMinY) mMinY = my;
|
||||
if (my > mMaxY) mMaxY = my;
|
||||
}
|
||||
}
|
||||
|
||||
for (const w of plan.widths) {
|
||||
// Label anchor fully off the wood footprint (axis-left), with margin for glyph half-width
|
||||
const margin = plan.fontPx * 0.55; // textAlign will grow away from wood; still keep anchor clear
|
||||
const onWood =
|
||||
w.labelAt[0] >= mMinX - 1 &&
|
||||
w.labelAt[0] <= mMaxX + 1 &&
|
||||
w.labelAt[1] >= mMinY - 1 &&
|
||||
w.labelAt[1] <= mMaxY + 1;
|
||||
assert.ok(
|
||||
!onWood,
|
||||
`width labelAt (${w.labelAt[0].toFixed(0)},${w.labelAt[1].toFixed(0)}) must be outside wood AABB`,
|
||||
);
|
||||
// Axis-left = past the left tip (smaller across); for this nearly-vertical slab, left of wood in x
|
||||
assert.ok(
|
||||
w.labelAt[0] < mMinX - 2,
|
||||
`width label ${w.label} x=${w.labelAt[0].toFixed(0)} must be left of wood minX=${mMinX.toFixed(0)} (margin~${margin.toFixed(0)})`,
|
||||
);
|
||||
// Width arrows still touch wood edges (left/right tips inside/on AABB)
|
||||
assert.ok(w.left[0] >= mMinX - 3 && w.left[0] <= mMaxX + 3);
|
||||
assert.ok(w.right[0] >= mMinX - 3 && w.right[0] <= mMaxX + 3);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user