diff --git a/library/packet/scripts/check-circular-layout.mjs b/library/packet/scripts/check-circular-layout.mjs index 7a28761af..f7f88f63f 100644 --- a/library/packet/scripts/check-circular-layout.mjs +++ b/library/packet/scripts/check-circular-layout.mjs @@ -128,15 +128,27 @@ if (four.stats.placed < 800) fails.push(`fourSides: only ${four.stats.placed} pi for (const n of nums) if (n <= lo) seqOk = false; if (nums.length) lo = Math.max(lo, ...nums); } - // monotone WITHIN a side = numbers increase in LABEL EMISSION order (ring order), - // which is the real placement order (four.pins is input order, so it cannot test this). - let monoOk = true; + // monotone WITHIN a side = tested against GEOMETRY, not the emission arrays (which + // numbering walks, so an emission-order check cannot fail — studio P2). Per classic4: + // L cy DESC (num↑ ⇒ screen y DOWN), R cy ASC (y UP), T cx ASC (x UP), B cx DESC (x DOWN). + // Pins are post-spread, so allow a drift tolerance = 60% of the side's mean step; a + // reversed sort (studio's poison) still fails, normal de-overlap drift does not. + const AXIS = { L: ['y', -1], R: ['y', +1], T: ['x', +1], B: ['x', -1] }; + let monoOk = true, monoWhy = ''; for (const sd of SEQ) { - const seq = four.labels.filter((l) => l.side === sd).map((l) => l.num); - for (let i = 1; i < seq.length; i++) if (seq[i] <= seq[i - 1]) { monoOk = false; break; } + const ps = four.pins.filter((p) => p.side === sd).sort((a, b) => a.num - b.num); + if (ps.length < 3) continue; + const [axis, dir] = AXIS[sd]; + const coords = ps.map((p) => p[axis] * dir); // expected non-decreasing + const span = coords[coords.length - 1] - coords[0]; + const tol = 0.6 * Math.abs(span) / (coords.length - 1); // 60% of mean step + let inversions = 0; + for (let i = 1; i < coords.length; i++) if (coords[i] < coords[i - 1] - tol) inversions++; + // a correct side has only small local drift inversions; a reversed sort inverts ~everything + if (inversions > coords.length * 0.2) { monoOk = false; monoWhy = `${sd}:${inversions}/${coords.length}`; } } if (!seqOk) fails.push('fourSides: numbers not in side sequence L !Array.isArray(l.polys) || l.polys.length !== 2).length; + if (badPolys !== 0) fails.push(`pairedRows: ${badPolys} leaders missing polys.length===2`); + note.push(`pairedRows check: crossings(recount) ${xr}, outside(recount) ${outPr}, leaders-with-2-polys ${paired.leaders.length - badPolys}/${paired.leaders.length}`); } // ── 3. sideLanes ──────────────────────────────────────────────────────────────