diff --git a/docs/CAPTURE.md b/docs/CAPTURE.md index 8535328..078e2e6 100644 --- a/docs/CAPTURE.md +++ b/docs/CAPTURE.md @@ -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. 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). -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. 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. - `sqft = (mask_pixels / scale²) / 144` (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 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. 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. 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`) | | Alpha | Kept through encode and upload | | 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 | Source below that floor: warn and require a new photo. No upscale from an undersized source. diff --git a/docs/PHOTOS.md b/docs/PHOTOS.md index f8adccc..375726b 100644 --- a/docs/PHOTOS.md +++ b/docs/PHOTOS.md @@ -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). 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). -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). 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. diff --git a/frontend/src/png/inventory.ts b/frontend/src/png/inventory.ts index f80f36c..2818198 100644 --- a/frontend/src/png/inventory.ts +++ b/frontend/src/png/inventory.ts @@ -1,7 +1,7 @@ import type { Mask, Source } from '../capture/capture.types'; 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_H = 4; diff --git a/frontend/src/png/overlay.ts b/frontend/src/png/overlay.ts index 6697d20..73eb341 100644 --- a/frontend/src/png/overlay.ts +++ b/frontend/src/png/overlay.ts @@ -45,6 +45,8 @@ export interface OverlayWidthMark { center: [number, number]; left: [number, number]; right: [number, number]; + /** Axis-left of the slab, outside the wood edge (opposite of length dim). */ + labelAt: [number, number]; label: string; } @@ -159,6 +161,7 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan { const { bottom, top, ux, uy } = oriented; const [rx, ry] = axisRightUnit(ux, uy); + const pad = Math.max(16, scale * 0.75); const stations = overlayWidthStations(lengthIn); const widths: OverlayWidthMark[] = []; 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 const leftSrc: [number, number] = [ox + rx * extent.minP, oy + ry * extent.minP]; 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; widths.push({ stationIn, center: mapToInventory(ox, oy), left: mapToInventory(leftSrc[0], leftSrc[1]), right: mapToInventory(rightSrc[0], rightSrc[1]), + labelAt: mapToInventory(labelSrcW[0], labelSrcW[1]), label: fmtInches(widthIn), }); } @@ -190,7 +197,6 @@ export function buildOverlayPlan(input: OverlayPlanInput): OverlayPlan { const extent = probeExtent(bitset, widthPx, heightPx, ox, oy, rx, ry); if (extent && extent.maxP > maxRight) maxRight = extent.maxP; } - const pad = Math.max(16, scale * 0.75); const outward = maxRight + pad; const aSrc: [number, number] = [bottom[0] + rx * outward, bottom[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 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 { stroke: '#000000', @@ -263,7 +269,7 @@ export function drawOverlayPlan( // Widths for (const w of plan.widths) { 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(); @@ -282,7 +288,7 @@ export function inventorySourceMap( outW: number, outH: number, ): InventoryMap { - const FILL = 0.8; + const FILL = 0.9; const x0 = Math.max(0, Math.round(slabBounds.x)); const y0 = Math.max(0, Math.round(slabBounds.y)); const x1 = Math.min(source.widthPx, x0 + Math.max(1, Math.round(slabBounds.w)));