diff --git a/tests/browser/pad-names.spec.js b/tests/browser/pad-names.spec.js
new file mode 100644
index 0000000..1f95701
--- /dev/null
+++ b/tests/browser/pad-names.spec.js
@@ -0,0 +1,90 @@
+// Pad labels in the canvas tooltip and the detail pane's pad table.
+// netdaq-small has a KiCad pad name on every pad, so a pad shows as the pin
+// the netlist names (U24:11 is "pin 11"). ciaa-acc has no names, so its pads
+// keep the old "pad N", N being the place in the footprint's pad list.
+
+const { test, expect } = require('@playwright/test');
+const fs = require('fs');
+const path = require('path');
+
+const BOARDS = {
+ named: {
+ page: 'file://' + path.join(__dirname, 'board.html'),
+ fixture: path.join(__dirname, '..', 'fixtures', 'netdaq-small.json'),
+ },
+ unnamed: {
+ page: 'file://' + path.join(__dirname, 'ciaa-acc.html'),
+ fixture: path.join(__dirname, '..', 'fixtures', 'ciaa-acc.json'),
+ },
+};
+
+function footprint(board, ref) {
+ const data = JSON.parse(fs.readFileSync(BOARDS[board].fixture, 'utf8'));
+ return data.pcbdata.footprints.find((fp) => fp.ref === ref);
+}
+
+async function load(page, board, ref) {
+ await page.goto('about:blank');
+ await page.goto(BOARDS[board].page + `#comp=${ref}&side=F&zoom=selected`);
+ await page.waitForFunction(() => window.__pcbaTest && window.__pcbaTest.ready(), null, { timeout: 60000 });
+ await page.waitForFunction(() => window.__pcbaTest.idle(), null, { timeout: 60000 });
+}
+
+// Move the pointer onto a front board point and return the tooltip text.
+async function hover(page, [x, y]) {
+ const v = await page.evaluate(() => window.__pcbaTest.viewBox('F'));
+ const box = await page.locator('#frontcanvas').boundingBox();
+ const fx = 0.5 + (x - v.cx) / v.w;
+ const fy = 0.5 + (y - v.cy) / v.h;
+ await page.mouse.move(box.x + box.width * fx, box.y + box.height * fy);
+ const tooltip = page.locator('#canvas-tooltip');
+ await expect(tooltip).toBeVisible();
+ return tooltip.innerText();
+}
+
+async function padTableNumbers(page) {
+ return page.locator('#detail-pane .pad-table tr td:first-child').allInnerTexts();
+}
+
+test('named board: hovering a pad shows its pin name, not its place in the list', async ({ page }) => {
+ const fp = footprint('named', 'U24');
+ // ibom sorts pads by name as text, so the third pad is pin 11.
+ expect(fp.pads[2].name).toBe('11');
+ await load(page, 'named', 'U24');
+
+ const text = await hover(page, fp.pads[2].pos);
+ expect(text).toContain('U24');
+ expect(text).toContain('pin 11');
+ expect(text).not.toContain('pad 3');
+
+ const pin1 = fp.pads.find((p) => p.pin1);
+ expect(await hover(page, pin1.pos)).toContain('pin ' + pin1.name);
+
+ expect(await padTableNumbers(page)).toEqual(fp.pads.map((p) => p.name));
+});
+
+test('unnamed board: hovering a pad still shows pad N', async ({ page }) => {
+ test.setTimeout(120000);
+ const fp = footprint('unnamed', 'U46');
+ expect(fp.pads.every((p) => p.name === undefined)).toBe(true);
+ expect(fp.pads[3].pin1).toBeFalsy();
+ await load(page, 'unnamed', 'U46');
+
+ const text = await hover(page, fp.pads[3].pos);
+ expect(text).toContain('U46');
+ expect(text).toContain('pad 4');
+ expect(text).not.toContain('pin 4');
+
+ expect(await padTableNumbers(page)).toEqual(fp.pads.map((p, i) => String(i + 1)));
+});
+
+test('the tooltip shows a pad name as text, not markup', async ({ page }) => {
+ await load(page, 'named', 'U24');
+ const fp = footprint('named', 'U24');
+ await page.evaluate(() => {
+ const fp = pcbdata.footprints.find((f) => f.ref === 'U24');
+ fp.pads[2].name = 'X';
+ });
+ const text = await hover(page, fp.pads[2].pos);
+ expect(text).toContain('pin X');
+});
diff --git a/web/app.js b/web/app.js
index 572e4a4..1563bbe 100644
--- a/web/app.js
+++ b/web/app.js
@@ -991,7 +991,8 @@ function renderDetailPane(fpIdx, showPads) {
var tr = document.createElement("tr");
var tdNum = document.createElement("td");
- tdNum.textContent = i + 1;
+ // The pad name when the export has one, else the place in the list.
+ tdNum.textContent = padName(pad) || (i + 1);
tr.appendChild(tdNum);
var tdNet = document.createElement("td");
diff --git a/web/render.js b/web/render.js
index 97766de..c15a272 100644
--- a/web/render.js
+++ b/web/render.js
@@ -1062,6 +1062,22 @@ function canvasHitScan(side, x, y) {
return null;
}
+// What the viewer calls pad j of a footprint. A pad name from the CAD export
+// ("11", "A1", "EP") is the pin as the netlist names it (REFDES:PIN), so it
+// reads "pin 11". Exports without names (stock ibom, Allegro) only have the
+// pad's place in the footprint's list, which is not the pin number, so that
+// reads "pad N".
+// The pad's name, or "" when it has none. Same test as the MCP tools, so
+// both agree on which pads have a pin name.
+function padName(pad) {
+ return typeof pad.name === "string" ? pad.name : "";
+}
+
+function padLabel(pad, j) {
+ if (padName(pad)) return "pin " + padName(pad);
+ return pad.pin1 ? "pin 1" : ("pad " + (j + 1));
+}
+
function padHitScan(layer, x, y) {
if (!layerShows(layer, "pads")) return null;
for (var i = 0; i < pcbdata.footprints.length; i++) {
@@ -1069,8 +1085,7 @@ function padHitScan(layer, x, y) {
for (var j = 0; j < fp.pads.length; j++) {
var pad = fp.pads[j];
if (pad.layers.includes(layer) && pointWithinPad(x, y, pad)) {
- var label = pad.pin1 ? "pin 1" : ("pad " + (j + 1));
- return { fpIdx: i, padLabel: label, net: pad.net };
+ return { fpIdx: i, padLabel: padLabel(pad, j), net: pad.net };
}
}
}
@@ -1266,7 +1281,7 @@ function handleMouseMove(e, layerdict) {
var comp = pcbdata.components[padHit.fpIdx];
var line1 = fp.ref + (comp && comp.val ? " \u2014 " + comp.val : "");
var line2 = padHit.padLabel + (padHit.net ? " \u2014 " + padHit.net : "");
- tooltip.innerHTML = "" + line1 + "
" + line2 + "";
+ tooltip.innerHTML = "" + escapeHtml(line1) + "
" + escapeHtml(line2) + "";
tooltip.style.display = "block";
tooltip.style.left = tipX + "px";
tooltip.style.top = tipY + "px";