fix(ui): a column is a RUN of pads, not two opposite ends of the board
The Arduino Uno came out wrong: 18 pins on the top header, 13 on the bottom, and SIX x values shared between the two. The column detector accepted any two pads sharing an x, so those pairs were read as columns and most of the board was thrown into the side gutters with leader lines fanning across the art. A column now needs three pads AND small gaps between them (no more than 40% of the body), which is what a real header looks like; rows get the same guard on x. The Uno lands as two headers, top and bottom, with no side gutters at all - covered by a test using its real 31-pin table.
This commit is contained in:
parent
4f68109421
commit
7dd9b90cf9
|
|
@ -206,6 +206,67 @@ describe('layoutInspectorPins — inset columns (the ReSpeaker Lite case)', () =
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
describe('layoutInspectorPins — the Arduino Uno (two opposite headers)', () => {
|
||||
// node_modules/@wokwi/elements arduino-uno-element pinInfo, board 274x208.
|
||||
// 18 pins along the top header (y=9) and 13 along the bottom (y=192), with
|
||||
// SIX x values shared between the two. Reading those pairs as columns threw
|
||||
// most of the board into the side gutters with leaders fanning across the
|
||||
// art; a column has to be a run of pads, not two opposite ends.
|
||||
const UNO: InspectorPinInput[] = [
|
||||
{ name: 'A5.2', x: 87, y: 9 },
|
||||
{ name: 'A4.2', x: 97, y: 9 },
|
||||
{ name: 'AREF', x: 106, y: 9 },
|
||||
{ name: 'GND.1', x: 115.5, y: 9 },
|
||||
{ name: '13', x: 125, y: 9 },
|
||||
{ name: '12', x: 134.5, y: 9 },
|
||||
{ name: '11', x: 144, y: 9 },
|
||||
{ name: '10', x: 153.5, y: 9 },
|
||||
{ name: '9', x: 163, y: 9 },
|
||||
{ name: '8', x: 173, y: 9 },
|
||||
{ name: '7', x: 189, y: 9 },
|
||||
{ name: '6', x: 198.5, y: 9 },
|
||||
{ name: '5', x: 208, y: 9 },
|
||||
{ name: '4', x: 217.5, y: 9 },
|
||||
{ name: '3', x: 227, y: 9 },
|
||||
{ name: '2', x: 236.5, y: 9 },
|
||||
{ name: '1', x: 246, y: 9 },
|
||||
{ name: '0', x: 255.5, y: 9 },
|
||||
{ name: 'IOREF', x: 131, y: 191.5 },
|
||||
{ name: 'RESET', x: 140.5, y: 191.5 },
|
||||
{ name: '3.3V', x: 150, y: 191.5 },
|
||||
{ name: '5V', x: 160, y: 191.5 },
|
||||
{ name: 'GND.2', x: 169.5, y: 191.5 },
|
||||
{ name: 'GND.3', x: 179, y: 191.5 },
|
||||
{ name: 'VIN', x: 188.5, y: 191.5 },
|
||||
{ name: 'A0', x: 208, y: 191.5 },
|
||||
{ name: 'A1', x: 217.5, y: 191.5 },
|
||||
{ name: 'A2', x: 227, y: 191.5 },
|
||||
{ name: 'A3', x: 236.5, y: 191.5 },
|
||||
{ name: 'A4', x: 246, y: 191.5 },
|
||||
{ name: 'A5', x: 255.5, y: 191.5 }
|
||||
];
|
||||
|
||||
const r = layoutInspectorPins(UNO, { width: 274, height: 208 });
|
||||
|
||||
it('puts every pin on the header it belongs to', () => {
|
||||
expect(r.pins).toHaveLength(31);
|
||||
expect(r.pins.filter((p) => p.edge === 'top')).toHaveLength(18);
|
||||
expect(r.pins.filter((p) => p.edge === 'bottom')).toHaveLength(13);
|
||||
});
|
||||
|
||||
it('uses no side gutters at all', () => {
|
||||
expect(r.pins.some((p) => p.edge === 'left' || p.edge === 'right')).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the shared-x pairs apart — one per header', () => {
|
||||
for (const name of ['5', '4', '3', '2', '1', '0']) {
|
||||
const p = r.pins.find((q) => q.name === name);
|
||||
if (p) expect(p.edge).toBe('top');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('signalKindOf', () => {
|
||||
it('maps the wokwi signal shapes', () => {
|
||||
expect(signalKindOf({ name: 'SDA', x: 0, y: 0, signals: [{ type: 'i2c', signal: 'SDA' }] })).toBe('i2c');
|
||||
|
|
|
|||
|
|
@ -187,12 +187,25 @@ export function layoutInspectorPins(
|
|||
const ys = pins.map((p) => p.y * scale);
|
||||
const side = new Array<PinEdge | null>(pins.length).fill(null);
|
||||
|
||||
/** Largest gap between consecutive values — a real header has small ones. */
|
||||
const maxGap = (vals: number[]): number => {
|
||||
const s = [...vals].sort((a, b) => a - b);
|
||||
let m = 0;
|
||||
for (let i = 1; i < s.length; i++) m = Math.max(m, s[i] - s[i - 1]);
|
||||
return m;
|
||||
};
|
||||
|
||||
// Vertical columns win first: they are the dominant shape on breakout
|
||||
// boards, shields and every DIP part.
|
||||
// boards, shields and every DIP part. A column must be a RUN of pads, which
|
||||
// is why it needs three of them and small gaps: an Arduino Uno has two
|
||||
// opposite headers with six x values shared between them, and reading those
|
||||
// pairs as columns threw most of the board's pins into the side gutters
|
||||
// with a fan of leader lines across the art.
|
||||
for (const g of cluster(xs)) {
|
||||
if (g.length < 2) continue;
|
||||
const spanY = Math.max(...g.map((i) => ys[i])) - Math.min(...g.map((i) => ys[i]));
|
||||
if (spanY < TOL) continue; // that is a row, not a column
|
||||
if (g.length < 3) continue;
|
||||
const gys = g.map((i) => ys[i]);
|
||||
if (Math.max(...gys) - Math.min(...gys) < TOL) continue; // a row, not a column
|
||||
if (maxGap(gys) > 0.4 * H) continue; // opposite ends of the body, not a run
|
||||
const cx = g.reduce((s, i) => s + xs[i], 0) / g.length;
|
||||
const e: PinEdge = cx <= W / 2 ? 'left' : 'right';
|
||||
for (const i of g) side[i] = e;
|
||||
|
|
@ -201,8 +214,9 @@ export function layoutInspectorPins(
|
|||
for (const g of cluster(ys)) {
|
||||
const free = g.filter((i) => side[i] === null);
|
||||
if (free.length < 2) continue;
|
||||
const spanX = Math.max(...free.map((i) => xs[i])) - Math.min(...free.map((i) => xs[i]));
|
||||
if (spanX < TOL) continue;
|
||||
const gxs = free.map((i) => xs[i]);
|
||||
if (Math.max(...gxs) - Math.min(...gxs) < TOL) continue;
|
||||
if (maxGap(gxs) > 0.4 * W) continue;
|
||||
const cy = free.reduce((s, i) => s + ys[i], 0) / free.length;
|
||||
const e: PinEdge = cy <= H / 2 ? 'top' : 'bottom';
|
||||
for (const i of free) side[i] = e;
|
||||
|
|
|
|||
Loading…
Reference in New Issue