From 6949cc4cee79cb433abf9415b48d97f48572a2a0 Mon Sep 17 00:00:00 2001 From: gherkin Date: Sun, 11 Oct 2026 18:43:48 +0200 Subject: [PATCH 1/2] Show pad names in the viewer (#96) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzHC7LspikUyimHw5ptN8x --- tests/browser/pad-names.spec.js | 90 +++++++++++++++++++++++++++++++++ web/app.js | 3 +- web/render.js | 15 ++++-- 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/browser/pad-names.spec.js 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..84a6a57 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, as in the canvas tooltip. + tdNum.textContent = pad.name || (i + 1); tr.appendChild(tdNum); var tdNet = document.createElement("td"); diff --git a/web/render.js b/web/render.js index 97766de..d029957 100644 --- a/web/render.js +++ b/web/render.js @@ -1062,6 +1062,16 @@ 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". +function padLabel(pad, j) { + if (pad.name) return "pin " + pad.name; + 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 +1079,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 +1275,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"; From 7dae0a53572b29d44709717aa2cebe807fe43103 Mon Sep 17 00:00:00 2001 From: gherkin Date: Sun, 11 Oct 2026 18:50:18 +0200 Subject: [PATCH 2/2] Pad name test matches the MCP tools padName() takes a pad name only when it is a string, as mcp/src/pads.js does, so the viewer and the tools agree on which pads have a pin name. Tooltip and pad table both use it. Fix the pad table comment: it does not match the tooltip for an unnamed pin-1 pad. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FzHC7LspikUyimHw5ptN8x --- web/app.js | 4 ++-- web/render.js | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/web/app.js b/web/app.js index 84a6a57..1563bbe 100644 --- a/web/app.js +++ b/web/app.js @@ -991,8 +991,8 @@ function renderDetailPane(fpIdx, showPads) { var tr = document.createElement("tr"); var tdNum = document.createElement("td"); - // The pad name when the export has one, as in the canvas tooltip. - tdNum.textContent = pad.name || (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 d029957..c15a272 100644 --- a/web/render.js +++ b/web/render.js @@ -1067,8 +1067,14 @@ function canvasHitScan(side, x, y) { // 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 (pad.name) return "pin " + pad.name; + if (padName(pad)) return "pin " + padName(pad); return pad.pin1 ? "pin 1" : ("pad " + (j + 1)); }