fix(photos): overlay UAT nits + 90% crop fill (#67,#68)
Length dim already mask tip-to-tip (locked by test). Width labels move axis-left off the wood via labelAt; font steps down to max(22, scale*1.0). Inventory/overlay crop FILL 0.8→0.9; CAPTURE/PHOTOS docs match.
This commit is contained in:
+4
-4
@@ -34,7 +34,7 @@ Otherwise the app opens Add photo.
|
|||||||
4. App draws the minimum-area bounding rectangle of the mask and the length axis on the long side of that rectangle. User rotates the axis if it does not follow the slab length, then approves the axis.
|
4. App draws the minimum-area bounding rectangle of the mask and the length axis on the long side of that rectangle. User rotates the axis if it does not follow the slab length, then approves the axis.
|
||||||
5. User enters length `L` in inches (1/8 in steps) and thickness (1/8 in steps), then presses Calculate.
|
5. User enters length `L` in inches (1/8 in steps) and thickness (1/8 in steps), then presses Calculate.
|
||||||
6. App sets `scale = longest_axis_px / L` (mask/wood span along the approved direction ÷ user `L`; rectangle is direction only).
|
6. App sets `scale = longest_axis_px / L` (mask/wood span along the approved direction ÷ user `L`; rectangle is direction only).
|
||||||
7. App computes widths, sqft, and bdft from the mask. App crops the masked slab into a 3:4 portrait frame: slab centered, 80% fill at the widest and tallest points, background transparent. App writes that frame as an inventory PNG (`image/png`, 8-bit RGBA).
|
7. App computes widths, sqft, and bdft from the mask. App crops the masked slab into a 3:4 portrait frame: slab centered, 90% fill at the widest and tallest points, background transparent. App writes that frame as an inventory PNG (`image/png`, 8-bit RGBA).
|
||||||
8. User sees the transparent PNG and the numbers. Milestone 1 stops here.
|
8. User sees the transparent PNG and the numbers. Milestone 1 stops here.
|
||||||
|
|
||||||
Calculate does not create a WooCommerce product.
|
Calculate does not create a WooCommerce product.
|
||||||
@@ -97,7 +97,7 @@ User enters length `L` in inches (1/8 in steps) and thickness (1/8 in steps), th
|
|||||||
- Widths: min, max, avg from samples taken every 6 in along the approved length axis, perpendicular to it, after dropping the first and last 6 in of the span (bulk widths, not tips). If `L ≤ 12 in` there are no interior stations and all three are 0.
|
- Widths: min, max, avg from samples taken every 6 in along the approved length axis, perpendicular to it, after dropping the first and last 6 in of the span (bulk widths, not tips). If `L ≤ 12 in` there are no interior stations and all three are 0.
|
||||||
- `sqft = (mask_pixels / scale²) / 144` (2 decimals)
|
- `sqft = (mask_pixels / scale²) / 144` (2 decimals)
|
||||||
- `bdft = sqft × thickness_in` (2 decimals)
|
- `bdft = sqft × thickness_in` (2 decimals)
|
||||||
- Crop: 3:4 portrait, slab centered, 80% fill at the widest and tallest points, transparent background
|
- Crop: 3:4 portrait, slab centered, 90% fill at the widest and tallest points, transparent background
|
||||||
- File: inventory PNG, `image/png`, 8-bit RGBA, alpha kept
|
- File: inventory PNG, `image/png`, 8-bit RGBA, alpha kept
|
||||||
|
|
||||||
User sees the transparent PNG and those numbers. Stop. Calculate does not create a WooCommerce product.
|
User sees the transparent PNG and those numbers. Stop. Calculate does not create a WooCommerce product.
|
||||||
@@ -108,7 +108,7 @@ On Calculate, the app builds the inventory image from the approved mask:
|
|||||||
|
|
||||||
1. Keep slab pixels from the source photo. Removed pixels are transparent.
|
1. Keep slab pixels from the source photo. Removed pixels are transparent.
|
||||||
2. Place the slab in a 3:4 portrait frame, centered.
|
2. Place the slab in a 3:4 portrait frame, centered.
|
||||||
3. Scale so the widest and tallest points of the slab fill 80% of the frame. Extra margin where the slab is narrower.
|
3. Scale so the widest and tallest points of the slab fill 90% of the frame. Extra margin where the slab is narrower.
|
||||||
4. After that crop, shorter side ≥ 800px when the source can supply it. Otherwise warn and require a new photo. No upscale from an undersized source.
|
4. After that crop, shorter side ≥ 800px when the source can supply it. Otherwise warn and require a new photo. No upscale from an undersized source.
|
||||||
5. Encode as PNG with alpha.
|
5. Encode as PNG with alpha.
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ The first inventory photo is the measurement reference. Later inventory photos u
|
|||||||
| Format | PNG, 8-bit RGBA (`image/png`) |
|
| Format | PNG, 8-bit RGBA (`image/png`) |
|
||||||
| Alpha | Kept through encode and upload |
|
| Alpha | Kept through encode and upload |
|
||||||
| Aspect | 3:4 portrait |
|
| Aspect | 3:4 portrait |
|
||||||
| Layout | Slab centered, 80% fill at the widest and tallest points |
|
| Layout | Slab centered, 90% fill at the widest and tallest points |
|
||||||
| Size | After crop, shorter side ≥ 800px when the source can supply it |
|
| Size | After crop, shorter side ≥ 800px when the source can supply it |
|
||||||
|
|
||||||
Source below that floor: warn and require a new photo. No upscale from an undersized source.
|
Source below that floor: warn and require a new photo. No upscale from an undersized source.
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ Phone capture of one slab → a lined-up set of Woo-compatible inventory PNGs, p
|
|||||||
1. User adds a batch of one or more photos of the same slab (Setup rules from Milestone 1: green or black sheet, top-down reference expectations still apply to the calibration choice).
|
1. User adds a batch of one or more photos of the same slab (Setup rules from Milestone 1: green or black sheet, top-down reference expectations still apply to the calibration choice).
|
||||||
2. User selects which photo in the batch is the calibration photo.
|
2. User selects which photo in the batch is the calibration photo.
|
||||||
3. App runs the Milestone 1 workflow on that calibration photo only: Mask → Length axis → Measure (length `L`, thickness, Calculate).
|
3. App runs the Milestone 1 workflow on that calibration photo only: Mask → Length axis → Measure (length `L`, thickness, Calculate).
|
||||||
4. After Calculate, the app applies the approved mask sheet and sensitivity, and the same crop rules (3:4 portrait, 80% fill, transparent PNG, shorter-side floor), to every other photo in the batch.
|
4. After Calculate, the app applies the approved mask sheet and sensitivity, and the same crop rules (3:4 portrait, 90% fill, transparent PNG, shorter-side floor), to every other photo in the batch.
|
||||||
5. App adds a second copy of the calibration inventory PNG with dimension overlays burned in (see Dimension overlay).
|
5. App adds a second copy of the calibration inventory PNG with dimension overlays burned in (see Dimension overlay).
|
||||||
6. User sees Photos ready: inventory PNGs lined up in this order — clean calibration PNG, then the dimension-overlay copy of the calibration PNG, then the other batch photos in capture order — plus the Milestone 1 calibration details (widths, sqft, bdft, scale) presented as in Measure.
|
6. User sees Photos ready: inventory PNGs lined up in this order — clean calibration PNG, then the dimension-overlay copy of the calibration PNG, then the other batch photos in capture order — plus the Milestone 1 calibration details (widths, sqft, bdft, scale) presented as in Measure.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Mask, Source } from '../capture/capture.types';
|
import type { Mask, Source } from '../capture/capture.types';
|
||||||
|
|
||||||
const FLOOR = 800; // shorter side, px
|
const FLOOR = 800; // shorter side, px
|
||||||
const FILL = 0.8; // 80% fill at the widest and tallest points of the slab
|
const FILL = 0.9; // 90% fill at the widest and tallest points of the slab
|
||||||
const ASPECT_W = 3;
|
const ASPECT_W = 3;
|
||||||
const ASPECT_H = 4;
|
const ASPECT_H = 4;
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export interface OverlayWidthMark {
|
|||||||
center: [number, number];
|
center: [number, number];
|
||||||
left: [number, number];
|
left: [number, number];
|
||||||
right: [number, number];
|
right: [number, number];
|
||||||
|
/** Axis-left of the slab, outside the wood edge (opposite of length dim). */
|
||||||
|
labelAt: [number, number];
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +161,7 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan {
|
|||||||
const { bottom, top, ux, uy } = oriented;
|
const { bottom, top, ux, uy } = oriented;
|
||||||
const [rx, ry] = axisRightUnit(ux, uy);
|
const [rx, ry] = axisRightUnit(ux, uy);
|
||||||
|
|
||||||
|
const pad = Math.max(16, scale * 0.75);
|
||||||
const stations = overlayWidthStations(lengthIn);
|
const stations = overlayWidthStations(lengthIn);
|
||||||
const widths: OverlayWidthMark[] = [];
|
const widths: OverlayWidthMark[] = [];
|
||||||
for (const stationIn of stations) {
|
for (const stationIn of stations) {
|
||||||
@@ -170,12 +173,16 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan {
|
|||||||
// left = opposite of axis-right; right = axis-right tip on slab edge
|
// left = opposite of axis-right; right = axis-right tip on slab edge
|
||||||
const leftSrc: [number, number] = [ox + rx * extent.minP, oy + ry * extent.minP];
|
const leftSrc: [number, number] = [ox + rx * extent.minP, oy + ry * extent.minP];
|
||||||
const rightSrc: [number, number] = [ox + rx * extent.maxP, oy + ry * extent.maxP];
|
const rightSrc: [number, number] = [ox + rx * extent.maxP, oy + ry * extent.maxP];
|
||||||
|
// Width number sits axis-left outside the slab (past left tip).
|
||||||
|
const labelOff = extent.minP - pad - scale * 0.4;
|
||||||
|
const labelSrcW: [number, number] = [ox + rx * labelOff, oy + ry * labelOff];
|
||||||
const widthIn = (extent.maxP - extent.minP + 1) / scale;
|
const widthIn = (extent.maxP - extent.minP + 1) / scale;
|
||||||
widths.push({
|
widths.push({
|
||||||
stationIn,
|
stationIn,
|
||||||
center: mapToInventory(ox, oy),
|
center: mapToInventory(ox, oy),
|
||||||
left: mapToInventory(leftSrc[0], leftSrc[1]),
|
left: mapToInventory(leftSrc[0], leftSrc[1]),
|
||||||
right: mapToInventory(rightSrc[0], rightSrc[1]),
|
right: mapToInventory(rightSrc[0], rightSrc[1]),
|
||||||
|
labelAt: mapToInventory(labelSrcW[0], labelSrcW[1]),
|
||||||
label: fmtInches(widthIn),
|
label: fmtInches(widthIn),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -190,7 +197,6 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan {
|
|||||||
const extent = probeExtent(bitset, widthPx, heightPx, ox, oy, rx, ry);
|
const extent = probeExtent(bitset, widthPx, heightPx, ox, oy, rx, ry);
|
||||||
if (extent && extent.maxP > maxRight) maxRight = extent.maxP;
|
if (extent && extent.maxP > maxRight) maxRight = extent.maxP;
|
||||||
}
|
}
|
||||||
const pad = Math.max(16, scale * 0.75);
|
|
||||||
const outward = maxRight + pad;
|
const outward = maxRight + pad;
|
||||||
const aSrc: [number, number] = [bottom[0] + rx * outward, bottom[1] + ry * outward];
|
const aSrc: [number, number] = [bottom[0] + rx * outward, bottom[1] + ry * outward];
|
||||||
const bSrc: [number, number] = [top[0] + rx * outward, top[1] + ry * outward];
|
const bSrc: [number, number] = [top[0] + rx * outward, top[1] + ry * outward];
|
||||||
@@ -198,7 +204,7 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan {
|
|||||||
// Nudge label further outward a bit
|
// Nudge label further outward a bit
|
||||||
const labelSrc: [number, number] = [midSrc[0] + rx * (scale * 0.4), midSrc[1] + ry * (scale * 0.4)];
|
const labelSrc: [number, number] = [midSrc[0] + rx * (scale * 0.4), midSrc[1] + ry * (scale * 0.4)];
|
||||||
|
|
||||||
const fontPx = Math.max(28, Math.round(scale * 1.2));
|
const fontPx = Math.max(22, Math.round(scale * 1.0));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stroke: '#000000',
|
stroke: '#000000',
|
||||||
@@ -263,7 +269,7 @@ export function drawOverlayPlan(
|
|||||||
// Widths
|
// Widths
|
||||||
for (const w of plan.widths) {
|
for (const w of plan.widths) {
|
||||||
arrow(w.left, w.right);
|
arrow(w.left, w.right);
|
||||||
ctx.fillText(w.label, w.center[0], w.center[1]);
|
ctx.fillText(w.label, w.labelAt[0], w.labelAt[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -282,7 +288,7 @@ export function inventorySourceMap(
|
|||||||
outW: number,
|
outW: number,
|
||||||
outH: number,
|
outH: number,
|
||||||
): InventoryMap {
|
): InventoryMap {
|
||||||
const FILL = 0.8;
|
const FILL = 0.9;
|
||||||
const x0 = Math.max(0, Math.round(slabBounds.x));
|
const x0 = Math.max(0, Math.round(slabBounds.x));
|
||||||
const y0 = Math.max(0, Math.round(slabBounds.y));
|
const y0 = Math.max(0, Math.round(slabBounds.y));
|
||||||
const x1 = Math.min(source.widthPx, x0 + Math.max(1, Math.round(slabBounds.w)));
|
const x1 = Math.min(source.widthPx, x0 + Math.max(1, Math.round(slabBounds.w)));
|
||||||
|
|||||||
Reference in New Issue
Block a user