[object Object]

← back to Dw Photo Capture

feat: front side auto-matches the design — hold steady on the pattern face → auto-fires /api/recognize (reuses the match path) instead of only prompting a flip; resets on code/open/close

761b356a73bec8321a9cf39c616ce1405de424f4 · 2026-07-06 15:37:00 -0700 · Steve Abrams

Files touched

Diff

commit 761b356a73bec8321a9cf39c616ce1405de424f4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 6 15:37:00 2026 -0700

    feat: front side auto-matches the design — hold steady on the pattern face → auto-fires /api/recognize (reuses the match path) instead of only prompting a flip; resets on code/open/close
---
 public/index.html | 23 +++++++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

diff --git a/public/index.html b/public/index.html
index a19c94b..2ce750f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -966,7 +966,7 @@ document.querySelectorAll('.chip').forEach(c=>c.addEventListener('click',()=>{
 }));
 // 🔢 Scan a printed mfr#/SKU with the camera → OCR (server Vision) → jump to that product.
 /* ===== LIVE scanner: stream the camera, OCR each frame, LOCK IN when a SKU repeats ===== */
-let scanStream=null,scanTimer=null,scanBusy=false,_lastTop=null,_lockHits=0;
+let scanStream=null,scanTimer=null,scanBusy=false,_lastTop=null,_lockHits=0,_frontHits=0;
 // shared camera choice (back/front) — drives BOTH the live scanner and every "Add Photo" capture input
 let camFacing=LS('camFacing')||'environment';
 function applyCamCapture(){   // point all <input capture> pickers at the chosen lens
@@ -1054,7 +1054,7 @@ async function openLiveScan(){
   unlockAudio();                              // we're inside the user's tap → unlock audio for the lock-in cue
   try{ await startScanStream(); }
   catch(e){ toast('Camera blocked — using snap'); return $('#scanInput').click(); }
-  _lastTop=null;_lockHits=0; $('#scanlive').classList.remove('locked'); $('#scanStatus').textContent='Point at the printed SKU…';
+  _lastTop=null;_lockHits=0;_frontHits=0; $('#scanlive').classList.remove('locked'); $('#scanStatus').textContent='Point at the printed SKU…';
   $('#scanFlip').textContent=(camFacing==='user'?'🔄 Back camera':'🔄 Front camera');
   $('#scanlive').hidden=false; document.body.style.overflow='hidden';
   scanTimer=setInterval(scanTick,600);
@@ -1065,7 +1065,7 @@ async function flipCam(){   // toggle back<->front, persist, re-acquire live + r
   if(!$('#scanlive').hidden){ try{ await startScanStream(); }catch(e){ toast('Could not switch camera'); } }
   else toast(camFacing==='user'?'📷 Front camera':'📷 Back camera');
 }
-function closeLiveScan(){ if(scanTimer)clearInterval(scanTimer); scanTimer=null; if(scanStream){scanStream.getTracks().forEach(t=>t.stop());scanStream=null;} $('#scanlive').hidden=true; $('#scanlive').classList.remove('locked'); document.body.style.overflow=''; }
+function closeLiveScan(){ if(scanTimer)clearInterval(scanTimer); scanTimer=null; if(scanStream){scanStream.getTracks().forEach(t=>t.stop());scanStream=null;} _frontHits=0; $('#scanlive').hidden=true; $('#scanlive').classList.remove('locked'); document.body.style.overflow=''; }
 async function ocrPost(dataUrl){ const r=await fetch('/api/ocr',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({dataUrl})}); return r.json(); }
 function mirrorDataUrl(dataUrl){ return new Promise((res,rej)=>{ const im=new Image(); im.onload=()=>{ const c=document.createElement('canvas'); c.width=im.naturalWidth; c.height=im.naturalHeight; const x=c.getContext('2d'); x.translate(c.width,0); x.scale(-1,1); x.drawImage(im,0,0); res(c.toDataURL('image/jpeg',0.8)); }; im.onerror=rej; im.src=dataUrl; }); }
 // front camera can deliver a MIRRORED frame (iOS) → backwards text reads as garbage. If a front-cam read finds nothing, retry flipped.
@@ -1085,6 +1085,7 @@ async function scanTick(){
     c.getContext('2d').drawImage(v,0,0,c.width,c.height);
     const d=await ocrSmart(c.toDataURL('image/jpeg',0.7));   // front-cam mirror-retry built in
     if(d&&d.top){
+      _frontHits=0;   // a real code appeared → not the pattern face; cancel any front streak
       if(d.top===_lastTop) _lockHits++; else {_lastTop=d.top;_lockHits=1;}
       // FAST LOCK: a strong read (known vendor prefix, or the brand name is printed on the
       // swatch and this is the biggest code) locks on the FIRST sighting; anything weaker
@@ -1106,7 +1107,21 @@ async function scanTick(){
         const frame=c.toDataURL('image/jpeg',0.8);   // keep the locked frame for a background logo-ID if no vendor
         setTimeout(()=>{ closeLiveScan(); applyScan(hit, frame); },420);  // hold the red a beat, then GO
       }
-    } else if(d && d.side==='front'){ _lastTop=null; _lockHits=0; $('#scanStatus').textContent='🎨 Front (pattern) · flip to the label to scan the SKU'; }
+    } else if(d && d.side==='front'){
+      // Pattern face — no SKU on this side. Hold steady a few frames, then auto-MATCH the design
+      // (reuse the recognize path) instead of dead-ending; user can still flip for the exact SKU.
+      _lastTop=null; _lockHits=0; _frontHits++;
+      if(_frontHits>=3){
+        $('#scanStatus').textContent='🎨 Matching this design…';
+        $('#scanlive').classList.add('locked');
+        try{navigator.vibrate&&navigator.vibrate(70);}catch(e){}
+        if(scanTimer){clearInterval(scanTimer);scanTimer=null;}
+        const frame=c.toDataURL('image/jpeg',0.85);
+        setTimeout(()=>{ closeLiveScan(); recognizeDataUrl(frame); },300);
+      } else {
+        $('#scanStatus').textContent='🎨 Front (pattern) · hold steady to match the design, or flip for the SKU';
+      }
+    }
     else if(_lockHits<2){ $('#scanStatus').textContent='📄 Point at the printed SKU…'; }
   }catch(e){} finally{ scanBusy=false; }
 }

← 9192328 feat: auto front/back side detection on scan — analyzeOcr cl  ·  back to Dw Photo Capture  ·  feat: structured field auto-detect (sku#/model#/name/color) 1ffca17 →