From f9b8ec86171b539e905df1d132ea500d126810bf Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 24 Sep 2026 15:36:04 -0400 Subject: [PATCH] Fix teeth vertex slider driving the lip vertex count opts() declared `verts` twice - once from the lip slider and again from the teeth slider. Duplicate keys in an object literal are silent in JS and the last one wins, so the teeth vertex control was quietly setting the lip vertex budget while the lip control did nothing at all. Renamed to teethVerts, with interior.js reading it under that name. selftest now parses the opts() literal and fails on duplicate keys, verified to catch this exact case. Co-Authored-By: Claude Opus 5 --- js/app.js | 4 ++-- js/interior.js | 2 +- js/selftest.js | 13 +++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/js/app.js b/js/app.js index a213979..673a56f 100644 --- a/js/app.js +++ b/js/app.js @@ -47,7 +47,7 @@ const opts = () => ({ tongueReject: +el('tongueReject').value / 100, blobGrow: +el('blobGrow').value, topBias: +el('topBias').value / 100, - verts: +el('teethVerts').value, + teethVerts: +el('teethVerts').value, smoothWin: +el('smoothWin').value, contourSmooth: +el('contourSmooth').value, apertureThresh: +el('apertureThresh').value / 1000, @@ -146,7 +146,7 @@ function measureAll(images, dense, o) { // Extraction keys on every knob that changes the pixels examined, so the cache // is keyed on exactly those and a change to anything else stays instant. const extractKey = (o) => - [o.cavityErode, o.tongueReject, o.blobGrow, o.topBias, o.verts].join('|'); + [o.cavityErode, o.tongueReject, o.blobGrow, o.topBias, o.teethVerts].join('|'); /* ---------- build ---------- */ diff --git a/js/interior.js b/js/interior.js index f3d7053..226475a 100644 --- a/js/interior.js +++ b/js/interior.js @@ -191,7 +191,7 @@ export function extractTeeth(img, innerNorm, ctx, o, wantDebug = false) { for (const i of comp.px) { only[i] = 1; cx += i % pw; cy += (i / pw) | 0; } cx /= comp.px.length; cy /= comp.px.length; - const local = radialContour(only, pw, ph, cx, cy, o.verts ?? 10); + const local = radialContour(only, pw, ph, cx, cy, o.teethVerts ?? 10); const contour = local.map((p) => ({ x: (p.x + px0) / W, y: (p.y + py0) / H })); return { diff --git a/js/selftest.js b/js/selftest.js index 24cae67..82abc7f 100644 --- a/js/selftest.js +++ b/js/selftest.js @@ -63,6 +63,19 @@ export async function runWiring() { if (list) { for (const m of list[1].matchAll(/'([\w]+)'/g)) { ids.add(m[1]); ids.add(m[1] + 'v'); } } + // Duplicate keys in an object literal are silent in JS - the last one wins. + // In opts() that meant the teeth vertex slider was quietly driving the lip + // vertex count while the lip slider did nothing at all. + const lit = app.match(/const opts = \(\) => \(\{([\s\S]*?)\n\}\);/); + if (lit) { + const keys = [...lit[1].matchAll(/^\s*([A-Za-z_$][\w$]*)\s*:/gm)].map((m) => m[1]); + const dupes = keys.filter((k, i) => keys.indexOf(k) !== i); + out.push({ name: `opts() has no duplicate keys (${keys.length} checked)`, + pass: dupes.length === 0, detail: [...new Set(dupes)].join(', ') }); + } else { + out.push({ name: 'opts() literal found for duplicate-key check', pass: false, detail: '' }); + } + const have = new Set([...html.matchAll(/id="([\w-]+)"/g)].map((m) => m[1])); const missing = [...ids].filter((i) => !have.has(i)); out.push({ name: `every el() id exists in index.html (${ids.size} checked)`,