diff --git a/packages/tryon-js/tests/fit.test.mjs b/packages/tryon-js/tests/fit.test.mjs index 01a8ecc..86fea33 100644 --- a/packages/tryon-js/tests/fit.test.mjs +++ b/packages/tryon-js/tests/fit.test.mjs @@ -42,3 +42,99 @@ test("compareFrames delta", () => { assert.equal(c.ok, true); assert.ok(c.width_delta_px > 0); }); + +// --- PD-to-fit-scale calculation tests (bounty #7) --- +test("pdToFitScale: correct scale for 140mm frame at 64mm PD, 120px PD", () => { + const s = overlaySize(frame, 120, 64); + const expected = (140 / 64) * 120 * 1.15; + assert.ok(Math.abs(s.overlayW - expected) < 0.5); + assert.ok(s.overlayH > 0); +}); + +test("pdToFitScale: larger PD means smaller overlay", () => { + const a = overlaySize(frame, 120, 74); + const b = overlaySize(frame, 120, 54); + assert.ok(b.overlayW > a.overlayW); +}); + +test("pdToFitScale: wider frame produces wider overlay", () => { + const wideFrame = { fit: { width_mm: 160 } }; + const narrowFrame = { fit: { width_mm: 120 } }; + const w = overlaySize(wideFrame, 120, 64); + const n = overlaySize(narrowFrame, 120, 64); + assert.ok(w.overlayW > n.overlayW); +}); + +test("pdToFitScale: proportional to pupil pixel distance", () => { + const a = overlaySize(frame, 80, 64); + const b = overlaySize(frame, 160, 64); + assert.ok(Math.abs(b.overlayW / a.overlayW - 2.0) < 0.1); +}); + +test("pdToFitScale: edge case — min PD (54mm)", () => { + const s = overlaySize({ fit: { width_mm: 140 } }, 100, 54); + assert.ok(s.overlayW > 0); + assert.ok(s.overlayW > 250); +}); + +test("pdToFitScale: edge case — max PD (74mm)", () => { + const s = overlaySize({ fit: { width_mm: 140 } }, 100, 74); + assert.ok(s.overlayW > 0); + assert.ok(s.overlayW < 250); +}); + +test("pdToFitScale: missing fit defaults to 140mm", () => { + const s = overlaySize({}, 120, 64); + const expected = (140 / 64) * 120 * 1.15; + assert.ok(Math.abs(s.overlayW - expected) < 0.5); +}); + +// --- Credit-card calibration math tests (bounty #7) --- +const CARD_W = 85.6; + +test("cardCal: pxPerMm from known object", () => { + const cardPx = 300; + const pxPerMm = cardPx / CARD_W; + assert.ok(Math.abs(pxPerMm - (300 / 85.6)) < 0.001); +}); + +test("cardCal: compute PD from card and pupil pixels", () => { + const cardPx = 300; + const pupilPx = 224; + const pdMm = pupilPx / (cardPx / CARD_W); + assert.ok(pdMm > 60 && pdMm < 68); +}); + +test("cardCal: larger card px means smaller estimated PD", () => { + const pupilPx = 200; + const pdSmallCard = pupilPx / (200 / CARD_W); + const pdLargeCard = pupilPx / (400 / CARD_W); + assert.ok(pdLargeCard < pdSmallCard); +}); + +test("cardCal: larger pupil px means larger estimated PD", () => { + const cardPx = 300; + const pxPerMm = cardPx / CARD_W; + assert.ok((300 / pxPerMm) > (100 / pxPerMm)); +}); + +test("cardCal: real-world PD ~64mm scenario", () => { + const cardPx = 250; + const pupilPx = 187; + const pdMm = pupilPx / (cardPx / CARD_W); + assert.ok(Math.abs(pdMm - 64) < 1.5); +}); + +test("cardCal: boundary — min PD through card", () => { + const cardPx = 500; + const pupilPx = 200; + const pdMm = pupilPx / (cardPx / CARD_W); + assert.ok(pdMm < 50); +}); + +test("cardCal: boundary — max PD through card", () => { + const cardPx = 100; + const pupilPx = 200; + const pdMm = pupilPx / (cardPx / CARD_W); + assert.ok(pdMm > 100); +}); diff --git a/packages/web/assets/app.css b/packages/web/assets/app.css index ef4d32b..5600ec5 100644 --- a/packages/web/assets/app.css +++ b/packages/web/assets/app.css @@ -143,3 +143,77 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text); font-size: 0.9rem; margin-top: 12px; } + +/* PD calibration wizard */ +.calibrate-btn { + margin-top: 8px; + width: 100%; + font-size: 12px; + padding: 6px 10px; + border-color: var(--accent2); + color: var(--accent2); + background: #0d1528; +} +.calibrate-btn.active { + background: #1a2744; + border-color: var(--accent); + color: var(--accent); +} +.cal-info { + margin-top: 8px; + padding: 8px 10px; + background: #0d1528; + border: 1px solid var(--line); + border-radius: 8px; + font-size: 11px; + line-height: 1.5; +} +.cal-info.hidden { display: none; } +.cal-info .cal-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} +.cal-info .cal-label { color: var(--muted); } +.cal-info .cal-value { color: var(--accent); font-weight: 600; } +.cal-info .cal-hint { + color: var(--muted); + font-size: 10px; + margin-top: 4px; + font-style: italic; +} +.cal-info .cal-actions { + display: flex; + gap: 6px; + margin-top: 8px; +} +.cal-info .cal-actions .btn { + font-size: 10px; + padding: 4px 8px; + flex: 1; +} +/* Calibration card overlay on stage */ +#cal-overlay { + position: absolute; + border: 2px dashed var(--accent); + border-radius: 8px; + background: rgba(245,197,24,0.08); + pointer-events: none; + display: none; + z-index: 3; + transition: none; +} +#cal-overlay.visible { display: block; } +#cal-overlay .cal-card-label { + position: absolute; + bottom: -22px; + left: 50%; + transform: translateX(-50%); + font-size: 10px; + color: var(--accent); + background: #000a; + padding: 2px 6px; + border-radius: 4px; + white-space: nowrap; +} diff --git a/packages/web/assets/app.js b/packages/web/assets/app.js index 06f0731..901937f 100644 --- a/packages/web/assets/app.js +++ b/packages/web/assets/app.js @@ -687,6 +687,7 @@ function loop() { ctx.fillText(t("hintIdle"), 40, h / 2); } if (mode !== "idle") drawGlasses(); + if (calMode) drawCardCalibrationOverlay(); raf = requestAnimationFrame(loop); } @@ -892,6 +893,11 @@ document.getElementById("btn-gallery").onclick = () => { }; document.getElementById("filter").onchange = (e) => loadCatalog(e.target.value); document.getElementById("pd").oninput = (e) => { + if (calMode) { + cardPx = Math.round(120 + (Number(e.target.value) - 54) * 13); + updateCalInfo(); + return; + } pdMm = Number(e.target.value) || 64; document.getElementById("pd-val").textContent = String(pdMm); updateMeta(); diff --git a/packages/web/index.html b/packages/web/index.html index 711d756..23e9ab9 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -82,6 +82,8 @@
Calibrate pupil distance for better fit scale
+ +