Improve the camera crop feature of the VIN photo capture flow. Currently there are two usability issues with the crop step after taking a VIN photo.
Current Behavior
Overlay/crop mismatch: The dashed rectangle guide overlay shown during camera capture ("Position VIN plate within the guide") does not match what the resulting cropped image looks like. The captured photo appears different from what was framed in the overlay.
Crop box is not adjustable: After capturing, the user can "Tap and drag to select crop area", but once the crop box is drawn it cannot be resized or repositioned. The user must start over if the crop selection is incorrect.
Expected Behavior
Consistent overlay-to-crop: The overlay guide shown during capture should accurately represent the area that will be cropped. What the user sees in the viewfinder should match the resulting image.
Adjustable crop box: After drawing an initial crop selection, the user should be able to:
Drag corner/edge handles to resize the crop box
Drag the crop box to reposition it
Confirm the crop when satisfied
Acceptance Criteria
Camera overlay guide accurately represents the crop area
Crop box supports resize via corner/edge handles after initial draw
Crop box supports repositioning via drag after initial draw
Works on both mobile and desktop viewports
OCR accuracy is not degraded by crop changes
Screenshots
See attached screenshots showing:
The crop screen after capture with "Tap and drag to select crop area" (non-adjustable)
The camera overlay with dashed guide rectangle during capture
## Summary
Improve the camera crop feature of the VIN photo capture flow. Currently there are two usability issues with the crop step after taking a VIN photo.
## Current Behavior
1. **Overlay/crop mismatch**: The dashed rectangle guide overlay shown during camera capture ("Position VIN plate within the guide") does not match what the resulting cropped image looks like. The captured photo appears different from what was framed in the overlay.
2. **Crop box is not adjustable**: After capturing, the user can "Tap and drag to select crop area", but once the crop box is drawn it cannot be resized or repositioned. The user must start over if the crop selection is incorrect.
## Expected Behavior
1. **Consistent overlay-to-crop**: The overlay guide shown during capture should accurately represent the area that will be cropped. What the user sees in the viewfinder should match the resulting image.
2. **Adjustable crop box**: After drawing an initial crop selection, the user should be able to:
- Drag corner/edge handles to resize the crop box
- Drag the crop box to reposition it
- Confirm the crop when satisfied
## Acceptance Criteria
- [ ] Camera overlay guide accurately represents the crop area
- [ ] Crop box supports resize via corner/edge handles after initial draw
- [ ] Crop box supports repositioning via drag after initial draw
- [ ] Works on both mobile and desktop viewports
- [ ] OCR accuracy is not degraded by crop changes
## Screenshots
See attached screenshots showing:
- The crop screen after capture with "Tap and drag to select crop area" (non-adjustable)
- The camera overlay with dashed guide rectangle during capture
The VIN camera crop has two root cause issues: (1) the GuidanceOverlay positions a centered 85%-width guide via CSS flexbox, but the CropTool defaults to {x:10, y:10, width:80, height:80} -- these systems share no coordinate data, so the crop area appears at the wrong position; (2) with VIN aspect ratio 6:1, the crop becomes a thin 13.33%-height strip at the top of the image with 24px touch targets, making handles nearly invisible on mobile.
The fix uses Approach A: calculate guidance-aware initial crop coordinates from the existing GUIDANCE_CONFIGS and pass them through to useImageCrop via the existing initialCrop prop. Touch targets are increased to 44px per Apple HIG and project conventions.
Planning Context
Decision Log
Decision
Reasoning Chain
Calculated initialCrop (Approach A)
Overlay uses CSS centering with known percentage values (85% width for VIN) -> same percentages can derive centered crop coordinates mathematically -> useImageCrop already accepts initialCrop prop -> minimal code changes with high confidence and no new abstractions
44px touch targets
Apple HIG minimum is 44px, project default-conventions.md requires >= 44px -> current 24px is approximately half the minimum -> mobile users cannot reliably grab handles -> increase to 44px touch area with 16px visual indicator
16px visual handle size (from 12px)
12px circle is difficult to see on high-DPI mobile screens -> 16px provides better visibility while 44px touch area covers interaction -> proportional increase without visual clutter
getInitialCropForGuidance() in types.ts
Function is pure (GuidanceType -> CropArea) with no dependencies -> co-locating with GUIDANCE_CONFIGS keeps configuration centralized -> avoids creating new utility file for a single function
No backend/OCR changes
executeCrop() in useImageCrop.ts produces the same JPEG 0.92 output regardless of initial crop position -> OCR preprocessing pipeline receives identical image format -> crop position change is frontend-only
Rejected Alternatives
Alternative
Why Rejected
DOM coordinate measurement (Approach B)
Requires measuring overlay DOM position relative to video element during capture transition -> race conditions between DOM measurement and stream stop -> fragile across screen sizes and orientations -> complexity not justified when mathematical derivation achieves the same result
Shared coordinate system (Approach C)
Would require refactoring GuidanceOverlay from CSS layout to percentage-based positioning -> creates coupling between viewfinder overlay (visual hint) and crop tool (interactive tool) -> over-engineers a solution that only needs a one-time coordinate bridge
Constraints & Assumptions
React 18 + MUI 6 + TypeScript strict mode
Touch targets >= 44px (default-conventions.md)
Mobile + desktop required (320px, 768px, 1920px viewports)
GUIDANCE_CONFIGS defines aspect ratios and overlay width percentages (types.ts:104-120)
Known Risks
Risk
Mitigation
Anchor
Overlay CSS centering may not produce exact same visual position as percentage-based crop
Accepted: CSS flexbox centering on a percentage-width box produces mathematically equivalent positioning to the percentage calculation. Both center a box of known proportions.
GuidanceOverlay.tsx:L50-L57 (flexbox center)
Larger touch targets may overlap on very thin VIN crop strip
Centered initial crop at ~14% height provides sufficient space for 44px handles. On 640px screen, 14% = ~90px, handles at corners with 44px do not overlap.
N/A - mathematical
executeCrop image quality unchanged
executeCrop uses cropArea percentages to calculate pixel coords on the source image. Changing initial position does not affect the drawImage or toBlob logic.
useImageCrop.ts:L262-L319
Invisible Knowledge
Architecture
types.ts
GUIDANCE_CONFIGS (aspectRatio, width%) ──> getInitialCropForGuidance()
|
v
CameraCapture.tsx CropArea (centered)
guidanceType ──> getInitialCropForGuidance() ──> initialCrop prop
|
v
CropTool.tsx useImageCrop(initialCrop)
Renders crop box at centered position
44px touch targets on all handles
Data Flow
Camera Viewfinder Crop Tool
GuidanceOverlay ─── (visual only, getInitialCropForGuidance()
CSS centered at no data flows) calculates matching
85% width, 6:1 AR ─────────────> centered CropArea
{x:7.5, y:42.92, w:85, h:14.17}
|
v
useImageCrop(initialCrop)
|
v
User adjusts with handles
|
v
executeCrop() -> Blob -> OCR
Invariants
getInitialCropForGuidance() output must produce a CropArea that, when rendered on the captured image, visually matches the GuidanceOverlay position from the viewfinder
Approximate vs pixel-exact positioning: Mathematical derivation from configuration values is simpler and more maintainable than DOM measurement, at the cost of not accounting for browser rendering quirks. Acceptable because the overlay is a guide hint, not a pixel-precise tool.
Larger handles increase visual noise but significantly improve mobile usability.
Milestones
Milestone 1: Bridge overlay position to crop initial coordinates
getInitialCropForGuidance lacks bounds validation -- if GUIDANCE_CONFIGS change, coordinates could exceed 0-100 range
aspectRatio === 1 falls through to height-constrained path -- should use >= 1
RULE 1 (HIGH):
3. At 320px viewport height, VIN crop strip is ~45px tall. With 44px corner handles, they nearly fill the entire strip height. Handles need to be clamped or use a responsive approach.
4. Diff line numbers are approximate (acceptable per diff-format spec -- context lines are the authoritative anchors)
RULE 2 (SHOULD_FIX):
5. Centering calculation (100 - dimension) / 2 duplicated in both paths -- minor, acceptable for a 2-path function
Plan Revisions Required
Add Math.min/Math.max bounds clamping to returned CropArea
Change config.aspectRatio > 1 to config.aspectRatio >= 1
Address 320px viewport: use responsive handle sizes (44px on screens > 400px, 32px on smaller) or accept that VIN crop on very small screens has tighter constraints
## QR Review: Plan Completeness + Plan Code
**Phase**: Plan-Review | **Agent**: Quality Reviewer | **Status**: NEEDS_CHANGES
### Findings
**RULE 0 (CRITICAL):**
1. `getInitialCropForGuidance` lacks bounds validation -- if GUIDANCE_CONFIGS change, coordinates could exceed 0-100 range
2. `aspectRatio === 1` falls through to height-constrained path -- should use `>= 1`
**RULE 1 (HIGH):**
3. At 320px viewport height, VIN crop strip is ~45px tall. With 44px corner handles, they nearly fill the entire strip height. Handles need to be clamped or use a responsive approach.
4. Diff line numbers are approximate (acceptable per diff-format spec -- context lines are the authoritative anchors)
**RULE 2 (SHOULD_FIX):**
5. Centering calculation `(100 - dimension) / 2` duplicated in both paths -- minor, acceptable for a 2-path function
### Plan Revisions Required
1. Add `Math.min`/`Math.max` bounds clamping to returned CropArea
2. Change `config.aspectRatio > 1` to `config.aspectRatio >= 1`
3. Address 320px viewport: use responsive handle sizes (44px on screens > 400px, 32px on smaller) or accept that VIN crop on very small screens has tighter constraints
*Verdict*: NEEDS_CHANGES | *Next*: Revise plan, re-review
RULE 0 Fix 1 -- Bounds validation: getInitialCropForGuidance now clamps all values to 0-100 range using Math.max(0, Math.min(100, value)). This guards against future GUIDANCE_CONFIGS changes producing out-of-bounds coordinates.
RULE 0 Fix 2 -- Aspect ratio boundary: Changed condition from config.aspectRatio > 1 to config.aspectRatio >= 1 so square (1:1) aspect ratios use the width-constrained path.
RULE 1 Fix 3 -- 320px viewport handle sizes: Use useTheme + useMediaQuery to detect small screens (< 400px height) and reduce handle size to 32px. This keeps handles within the VIN crop strip on very small viewports while maintaining 44px on normal mobile and desktop.
Apple HIG requires 44px minimum touch targets -> but VIN crop strip at 320px viewport height is only ~45px tall -> 44px handles would nearly fill entire strip -> use useMediaQuery('(max-height: 400px)') to detect compact viewports -> 32px handles on compact, 44px on standard -> maintains usability without overflowing
Bounds clamping via clampPct
GUIDANCE_CONFIGS values could change in future -> unclamped arithmetic could produce negative or >100 percentages -> explicit clamping guards against invalid crop coordinates at trivial cost
aspectRatio >= 1 (from > 1)
Edge case: 1:1 square guidance type -> > 1 sends to height-constrained path which uses 70% height -> >= 1 sends to width-constrained path using 85% width -> width-constrained is more appropriate for square guides on wide screens
Updated Test Scenarios (M1)
Add to existing scenarios:
Edge: All CropArea values are within 0-100 range (validated by clampPct)
All three RULE 0/1 findings addressed. Plan structure, decision log, tests, and risks verified complete.
TW Plan-Scrub: PASS (with 2 minor corrections)
CameraCapture.tsx comment (intent leakage): "Crop initial position matches guidance overlay center" -> "Calculate crop coordinates centered on the guidance overlay area"
CropTool.tsx (missing WHY): Add comment for edge handle proportions: "Edge handles: 70% of touch target for horizontal bars, 75% for vertical bars"
Both corrections will be applied during execution.
QR Plan-Code: Addressed in revision
All RULE 0/1/2 findings resolved. No remaining blockers.
Verdict: PLAN APPROVED | Next: Create branch, begin execution
## Plan Review Results
**Phase**: Plan-Review | **Agent**: Multiple | **Status**: PASS
### QR Plan-Completeness Re-Review: PASS
All three RULE 0/1 findings addressed. Plan structure, decision log, tests, and risks verified complete.
### TW Plan-Scrub: PASS (with 2 minor corrections)
1. **CameraCapture.tsx comment** (intent leakage): "Crop initial position matches guidance overlay center" -> "Calculate crop coordinates centered on the guidance overlay area"
2. **CropTool.tsx** (missing WHY): Add comment for edge handle proportions: "Edge handles: 70% of touch target for horizontal bars, 75% for vertical bars"
Both corrections will be applied during execution.
### QR Plan-Code: Addressed in revision
All RULE 0/1/2 findings resolved. No remaining blockers.
*Verdict*: PLAN APPROVED | *Next*: Create branch, begin execution
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Improve the camera crop feature of the VIN photo capture flow. Currently there are two usability issues with the crop step after taking a VIN photo.
Current Behavior
Overlay/crop mismatch: The dashed rectangle guide overlay shown during camera capture ("Position VIN plate within the guide") does not match what the resulting cropped image looks like. The captured photo appears different from what was framed in the overlay.
Crop box is not adjustable: After capturing, the user can "Tap and drag to select crop area", but once the crop box is drawn it cannot be resized or repositioned. The user must start over if the crop selection is incorrect.
Expected Behavior
Consistent overlay-to-crop: The overlay guide shown during capture should accurately represent the area that will be cropped. What the user sees in the viewfinder should match the resulting image.
Adjustable crop box: After drawing an initial crop selection, the user should be able to:
Acceptance Criteria
Screenshots
See attached screenshots showing:
Plan: Improve VIN Photo Capture Camera Crop
Phase: Planning | Agent: Planner | Status: AWAITING_REVIEW
Overview
The VIN camera crop has two root cause issues: (1) the GuidanceOverlay positions a centered 85%-width guide via CSS flexbox, but the CropTool defaults to
{x:10, y:10, width:80, height:80}-- these systems share no coordinate data, so the crop area appears at the wrong position; (2) with VIN aspect ratio 6:1, the crop becomes a thin 13.33%-height strip at the top of the image with 24px touch targets, making handles nearly invisible on mobile.The fix uses Approach A: calculate guidance-aware initial crop coordinates from the existing
GUIDANCE_CONFIGSand pass them through touseImageCropvia the existinginitialCropprop. Touch targets are increased to 44px per Apple HIG and project conventions.Planning Context
Decision Log
useImageCropalready acceptsinitialCropprop -> minimal code changes with high confidence and no new abstractionsgetInitialCropForGuidance()in types.tsexecuteCrop()inuseImageCrop.tsproduces the same JPEG 0.92 output regardless of initial crop position -> OCR preprocessing pipeline receives identical image format -> crop position change is frontend-onlyRejected Alternatives
Constraints & Assumptions
useImageCropalready accepts optionalinitialCropparameter (useImageCrop.ts:11)GUIDANCE_CONFIGSdefines aspect ratios and overlay width percentages (types.ts:104-120)Known Risks
executeCropimage quality unchangedexecuteCropusescropAreapercentages to calculate pixel coords on the source image. Changing initial position does not affect the drawImage or toBlob logic.Invisible Knowledge
Architecture
Data Flow
Invariants
getInitialCropForGuidance()output must produce a CropArea that, when rendered on the captured image, visually matches the GuidanceOverlay position from the viewfinderTradeoffs
Milestones
Milestone 1: Bridge overlay position to crop initial coordinates
Files:
frontend/src/shared/components/CameraCapture/types.ts,frontend/src/shared/components/CameraCapture/CameraCapture.tsx,frontend/src/shared/components/CameraCapture/CropTool.tsxRequirements:
getInitialCropForGuidance(type: GuidanceType): CropArea | undefinedto types.ts{x: 7.5, y: 42.92, width: 85, height: 14.17}{x: 26.67, y: 15, width: 46.67, height: 70}{x: 22.95, y: 15, width: 54.09, height: 70}initialCropoptional prop to CropToolPropsAcceptance Criteria:
Tests:
frontend/src/shared/components/CameraCapture/__tests__/getInitialCropForGuidance.test.tsCode Changes:
Milestone 2: Improve crop handle touch targets for mobile
Files:
frontend/src/shared/components/CameraCapture/CropTool.tsxFlags: needs conformance check (44px touch target requirement)
Requirements:
Acceptance Criteria:
Tests:
Code Changes:
Milestone 3: Documentation
Files:
frontend/src/shared/components/CameraCapture/README.md(if needed)Requirements:
Acceptance Criteria:
Source Material: Invisible Knowledge section of this plan
Milestone Dependencies
M2 depends on M1 because correct crop positioning must exist before handle improvements matter. M3 depends on both being complete.
Verdict: AWAITING_REVIEW | Next: Plan review cycle (QR plan-completeness -> TW plan-scrub -> QR plan-code -> QR plan-docs)
QR Review: Plan Completeness + Plan Code
Phase: Plan-Review | Agent: Quality Reviewer | Status: NEEDS_CHANGES
Findings
RULE 0 (CRITICAL):
getInitialCropForGuidancelacks bounds validation -- if GUIDANCE_CONFIGS change, coordinates could exceed 0-100 rangeaspectRatio === 1falls through to height-constrained path -- should use>= 1RULE 1 (HIGH):
3. At 320px viewport height, VIN crop strip is ~45px tall. With 44px corner handles, they nearly fill the entire strip height. Handles need to be clamped or use a responsive approach.
4. Diff line numbers are approximate (acceptable per diff-format spec -- context lines are the authoritative anchors)
RULE 2 (SHOULD_FIX):
5. Centering calculation
(100 - dimension) / 2duplicated in both paths -- minor, acceptable for a 2-path functionPlan Revisions Required
Math.min/Math.maxbounds clamping to returned CropAreaconfig.aspectRatio > 1toconfig.aspectRatio >= 1Verdict: NEEDS_CHANGES | Next: Revise plan, re-review
Plan Revision: Addressing QR Findings
Phase: Planning | Agent: Planner | Status: AWAITING_REVIEW
Changes from QR Review
RULE 0 Fix 1 -- Bounds validation:
getInitialCropForGuidancenow clamps all values to 0-100 range usingMath.max(0, Math.min(100, value)). This guards against future GUIDANCE_CONFIGS changes producing out-of-bounds coordinates.RULE 0 Fix 2 -- Aspect ratio boundary: Changed condition from
config.aspectRatio > 1toconfig.aspectRatio >= 1so square (1:1) aspect ratios use the width-constrained path.RULE 1 Fix 3 -- 320px viewport handle sizes: Use
useTheme+useMediaQueryto detect small screens (< 400px height) and reduce handle size to 32px. This keeps handles within the VIN crop strip on very small viewports while maintaining 44px on normal mobile and desktop.Revised Code: getInitialCropForGuidance (types.ts)
Revised Code: Responsive Handle Sizes (CropTool.tsx)
Handle component updated to use passed config instead of hardcoded values:
CropHandle component accepts config prop:
Updated Decision Log Entry
useMediaQuery('(max-height: 400px)')to detect compact viewports -> 32px handles on compact, 44px on standard -> maintains usability without overflowing> 1sends to height-constrained path which uses 70% height ->>= 1sends to width-constrained path using 85% width -> width-constrained is more appropriate for square guides on wide screensUpdated Test Scenarios (M1)
Add to existing scenarios:
Updated Known Risks
Verdict: AWAITING_REVIEW | Next: Re-review QR plan-completeness
Plan Review Results
Phase: Plan-Review | Agent: Multiple | Status: PASS
QR Plan-Completeness Re-Review: PASS
All three RULE 0/1 findings addressed. Plan structure, decision log, tests, and risks verified complete.
TW Plan-Scrub: PASS (with 2 minor corrections)
Both corrections will be applied during execution.
QR Plan-Code: Addressed in revision
All RULE 0/1/2 findings resolved. No remaining blockers.
Verdict: PLAN APPROVED | Next: Create branch, begin execution
egullickson referenced this issue2026-02-11 03:04:24 +00:00