[object Object]

← back to Dw Photo Capture

Calibrate-once measurement: card sizes px/in for the device once → stored in localStorage → every later scan just needs the goods box, no card. Zoom locked so focal length stays constant. Recalibrate button to re-anchor.

a2500f825754a1c70e09db378952ed6240876125 · 2026-07-06 21:43:31 -0700 · Steve Abrams

Files touched

Diff

commit a2500f825754a1c70e09db378952ed6240876125
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 6 21:43:31 2026 -0700

    Calibrate-once measurement: card sizes px/in for the device once → stored in localStorage → every later scan just needs the goods box, no card. Zoom locked so focal length stays constant. Recalibrate button to re-anchor.
---
 public/index.html | 39 ++++++++++++++++++++++++++++++++-------
 1 file changed, 32 insertions(+), 7 deletions(-)

diff --git a/public/index.html b/public/index.html
index d6f3dfb..e25f8d0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -446,7 +446,12 @@
   <div class="mbox mcard" id="mCard"><span class="mlbl">💳 card</span><i class="mrz"></i></div>
   <div class="mbox mgoods" id="mSample"><span class="mlbl">📦 goods</span><i class="mrz"></i></div>
   <div class="mreadout" id="mReadout">Size 💳 to a credit card, 📦 to the goods</div>
-  <div class="mbar"><button id="mUse">✓ Use W×L</button><button id="mClose">✕ Close</button></div>
+  <div class="mbar">
+    <button id="mSaveCal" hidden>✓ Save calibration</button>
+    <button id="mUse" hidden>✓ Use W×L</button>
+    <button id="mRecal" hidden>↺ Recalibrate</button>
+    <button id="mClose">✕ Close</button>
+  </div>
 </div>
 <script>
 const $=s=>document.querySelector(s), grid=$('#grid');
@@ -1612,10 +1617,23 @@ $('#addVoice').addEventListener('click',addPromptMissing);   // manual re-trigge
 
 // ── Live dimension measure: a credit card (3.375" wide) in frame calibrates pixels→inches ──
 const CARD_IN=3.375;   // ISO/IEC 7810 ID-1 (credit card) width
-let _mStream=null,_measW=0,_measL=0;
-function mRecompute(){ const c=$('#mCard'),s=$('#mSample'); const ppi=c.offsetWidth/CARD_IN;
-  _measW=s.offsetWidth/ppi; _measL=s.offsetHeight/ppi;
-  $('#mReadout').innerHTML=`W <b>${_measW.toFixed(1)}"</b> · L <b>${_measL.toFixed(1)}"</b> <small>· card=3.375"</small>`; }
+let _mStream=null,_measW=0,_measL=0,_ppi=0,_mMode='measure';
+// CALIBRATE ONCE: a card sizes px→inches for THIS device; stored in localStorage, reused forever.
+function calGet(){ try{ return JSON.parse(localStorage.getItem('dwMeasureCal')||'null'); }catch(e){ return null; } }
+function calSet(o){ try{ localStorage.setItem('dwMeasureCal',JSON.stringify(o)); }catch(e){} }
+function mSetMode(mode){ _mMode=mode;
+  $('#mCard').hidden = mode!=='calibrate';   // card only shown while calibrating
+  $('#mSample').hidden = mode==='calibrate';
+  $('#mSaveCal').hidden = mode!=='calibrate';
+  $('#mUse').hidden = mode==='calibrate';
+  $('#mRecal').hidden = mode==='calibrate';
+  mRecompute(); }
+function mRecompute(){
+  if(_mMode==='calibrate'){ _ppi=$('#mCard').offsetWidth/CARD_IN;
+    $('#mReadout').innerHTML=`One-time setup: size 💳 to a real credit card, then Save. <small>(${_ppi.toFixed(0)} px/in)</small>`; return; }
+  const s=$('#mSample'); if(!_ppi){ $('#mReadout').textContent='Tap ↺ Recalibrate first.'; return; }
+  _measW=s.offsetWidth/_ppi; _measL=s.offsetHeight/_ppi;
+  $('#mReadout').innerHTML=`W <b>${_measW.toFixed(1)}"</b> · L <b>${_measL.toFixed(1)}"</b> <small>· saved calibration · hold at your usual distance</small>`; }
 function mMakeBox(el){ let mode=null,sx,sy,ox,oy,ow,oh; const rz=el.querySelector('.mrz');
   const start=(e,m)=>{ mode=m; sx=e.clientX; sy=e.clientY; ox=el.offsetLeft; oy=el.offsetTop; ow=el.offsetWidth; oh=el.offsetHeight; (m==='resize'?rz:el).setPointerCapture(e.pointerId); e.preventDefault(); e.stopPropagation(); };
   const move=e=>{ if(!mode)return; const dx=e.clientX-sx,dy=e.clientY-sy;
@@ -1635,12 +1653,19 @@ async function openMeasure(){
   try{ const tr=_mStream.getVideoTracks()[0], caps=tr.getCapabilities?tr.getCapabilities():{};
     if(caps.zoom) await tr.applyConstraints({advanced:[{zoom:caps.zoom.min||1}]}); }catch(e){}
   $('#mVideo').srcObject=_mStream; $('#measureModal').hidden=false; document.body.style.overflow='hidden';
-  const C=$('#mCard'),S=$('#mSample'); Object.assign(C.style,{left:'26px',top:'96px',width:'135px',height:'85px'});
-  Object.assign(S.style,{left:'56px',top:'220px',width:'210px',height:'270px'}); mRecompute(); }
+  Object.assign($('#mCard').style,{left:'26px',top:'96px',width:'135px',height:'85px'});
+  Object.assign($('#mSample').style,{left:'56px',top:'200px',width:'210px',height:'270px'});
+  const cal=calGet();
+  if(cal&&cal.ppi){ _ppi=cal.ppi; mSetMode('measure'); }   // reuse saved calibration — no card
+  else mSetMode('calibrate'); }                             // first time on this device → calibrate
 function closeMeasure(){ if(_mStream){ _mStream.getTracks().forEach(t=>t.stop()); _mStream=null; } $('#measureModal').hidden=true; document.body.style.overflow=''; }
 mMakeBox($('#mCard')); mMakeBox($('#mSample'));
 $('#addMeasure').addEventListener('click',openMeasure);
 $('#mClose').addEventListener('click',closeMeasure);
+$('#mRecal').addEventListener('click',()=>mSetMode('calibrate'));
+$('#mSaveCal').addEventListener('click',()=>{ mRecompute();
+  if(_ppi>0){ calSet({ppi:_ppi, sw:window.innerWidth, ts:Date.now()});
+    toast('✓ Calibrated ('+_ppi.toFixed(0)+' px/in) — no card needed next time'); mSetMode('measure'); } });
 $('#mUse').addEventListener('click',()=>{ const w=_measW.toFixed(1)+'"',l=_measL.toFixed(1)+'"';
   _addExtracted.width=w; _addExtracted.roll_length=l;
   $('#addSpecs').insertAdjacentHTML('afterbegin',`<div class="add-spec"><span>📐 Measured</span><b>${w} × ${l}</b></div>`);

← 8cff6c4 Camera-unavailable UX: detect Chrome/3rd-party iOS browsers  ·  back to Dw Photo Capture  ·  yolo hardening (8 fixes from adversarial review): C1 visual 1570feb →