[object Object]

← back to Dw Photo Capture

Card-free measurement groundwork: lock camera zoom + /caps capability probe (reports focusDistance/zoom/motion support per device) to pick the right no-reference scale method

da94b6f8029af76e36fe8732a8ef1b6ede88de42 · 2026-07-06 21:22:09 -0700 · Steve Abrams

Files touched

Diff

commit da94b6f8029af76e36fe8732a8ef1b6ede88de42
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 6 21:22:09 2026 -0700

    Card-free measurement groundwork: lock camera zoom + /caps capability probe (reports focusDistance/zoom/motion support per device) to pick the right no-reference scale method
---
 public/caps.html  | 48 ++++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html |  3 +++
 server.js         | 12 ++++++++++++
 3 files changed, 63 insertions(+)

diff --git a/public/caps.html b/public/caps.html
new file mode 100644
index 0000000..6c1ab69
--- /dev/null
+++ b/public/caps.html
@@ -0,0 +1,48 @@
+<!doctype html><html><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
+<title>DW camera capability probe</title>
+<style>
+  body{margin:0;background:#0b0a08;color:#eee;font:15px/1.5 -apple-system,system-ui,sans-serif;padding:18px}
+  h1{font-size:18px;margin:0 0 4px}.sub{color:#9a917d;font-size:13px;margin-bottom:14px}
+  #v{width:100%;border-radius:12px;background:#000;margin-bottom:12px}
+  .row{display:flex;justify-content:space-between;gap:10px;padding:9px 0;border-bottom:1px solid #ffffff14;font-size:14px}
+  .k{color:#b7ad98}.y{color:#2bd06a;font-weight:700}.n{color:#ff6a6a;font-weight:700}
+  button{width:100%;padding:14px;margin-top:14px;border:none;border-radius:12px;background:#2bd06a;color:#032;font-size:16px;font-weight:700}
+  #done{color:#2bd06a;text-align:center;margin-top:12px;font-weight:700;display:none}
+</style></head><body>
+<h1>DW camera probe</h1>
+<div class="sub">Point the back camera at anything, then tap “Send to Claude”. This tells us if your device can measure size by moving the phone (no card).</div>
+<video id="v" autoplay playsinline muted></video>
+<div id="out"></div>
+<button id="send">Send to Claude</button>
+<div id="done">✓ Sent — tell Claude “probe done”.</div>
+<script>
+const $=s=>document.querySelector(s); let CAPS={};
+function row(k,ok,val){ return `<div class="row"><span class="k">${k}</span><span class="${ok?'y':'n'}">${val}</span></div>`; }
+(async()=>{
+  const rep={ua:navigator.userAgent, motion:typeof DeviceMotionEvent!=='undefined',
+    motionPerm:(typeof DeviceMotionEvent!=='undefined' && typeof DeviceMotionEvent.requestPermission==='function')};
+  try{
+    const st=await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:'environment'}}});
+    $('#v').srcObject=st; const tr=st.getVideoTracks()[0];
+    const caps=tr.getCapabilities?tr.getCapabilities():{}, set=tr.getSettings?tr.getSettings():{};
+    rep.resolution=(set.width||'?')+'x'+(set.height||'?');
+    rep.zoom=caps.zoom?JSON.stringify(caps.zoom):null;
+    rep.focusDistance=caps.focusDistance?JSON.stringify(caps.focusDistance):null;
+    rep.focusDistanceNow=(set.focusDistance!==undefined)?set.focusDistance:null;
+    rep.focusMode=caps.focusMode?JSON.stringify(caps.focusMode):null;
+    rep.capsKeys=Object.keys(caps);
+  }catch(e){ rep.cameraErr=e.message; }
+  CAPS=rep;
+  $('#out').innerHTML =
+    row('Camera', !rep.cameraErr, rep.cameraErr?('ERR '+rep.cameraErr):(rep.resolution||'ok'))+
+    row('Focus distance (→ card-free!)', !!rep.focusDistance, rep.focusDistance?('YES '+rep.focusDistance):'no')+
+    row('Live focus distance', rep.focusDistanceNow!=null, rep.focusDistanceNow!=null?String(rep.focusDistanceNow):'no')+
+    row('Zoom control', !!rep.zoom, rep.zoom?('yes '+rep.zoom):'no')+
+    row('Motion (parallax fallback)', rep.motion, rep.motion?(rep.motionPerm?'yes (needs tap)':'yes'):'no');
+})();
+$('#send').addEventListener('click',async()=>{
+  try{ await fetch('/api/caps',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(CAPS)}); }catch(e){}
+  $('#done').style.display='block'; $('#send').style.display='none';
+});
+</script></body></html>
diff --git a/public/index.html b/public/index.html
index 6ead998..f25bf7c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1628,6 +1628,9 @@ function mMakeBox(el){ let mode=null,sx,sy,ox,oy,ow,oh; const rz=el.querySelecto
   el.addEventListener('pointerup',end); rz.addEventListener('pointerup',end); }
 async function openMeasure(){ try{ _mStream=await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:'environment'}}}); }
   catch(e){ toast('Camera unavailable for measuring'); return; }
+  // LOCK ZOOM to minimum so the focal length / field-of-view stays constant (scale depends on it)
+  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(); }
diff --git a/server.js b/server.js
index 8b754d7..8264ebb 100644
--- a/server.js
+++ b/server.js
@@ -819,6 +819,18 @@ const appHandler = (req, res) => {
   // Status snapshot (polling fallback if a client's SSE is down).
   if (u.pathname === '/api/pair/status' && req.method === 'GET') return send(res, 200, pairStatus());
 
+  // Camera capability probe page + its result logger (learn what an iPhone/iPad Safari exposes).
+  if (u.pathname === '/caps') {
+    const f = path.join(ROOT, 'public/caps.html');
+    if (!fs.existsSync(f)) return send(res, 404, { err: 'caps.html missing' });
+    return res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }), res.end(fs.readFileSync(f, 'utf8'));
+  }
+  if (u.pathname === '/api/caps' && req.method === 'POST') {
+    let body = ''; req.on('data', c => { body += c; if (body.length > 64 * 1024) req.destroy(); });
+    req.on('end', () => { try { fs.appendFileSync(path.join(ROOT, 'caps.log'), new Date().toISOString() + ' ' + body + '\n'); } catch (e) {} send(res, 200, { ok: true }); });
+    return;
+  }
+
   // Desktop control page + phone live-camera page (both behind the existing basic-auth).
   if (u.pathname === '/desk') {
     const f = path.join(ROOT, 'public/desk.html');

← b87d402 Visual pattern-ID in capture flow (front photo → identify-mu  ·  back to Dw Photo Capture  ·  Handoff doc: how to wire the fine-tuned DW CLIP weights into b0a3ecc →