From 7c1639e751ba04930d9720e87d74298a557b030b Mon Sep 17 00:00:00 2001 From: gherkin Date: Tue, 27 Oct 2026 21:09:09 +0100 Subject: [PATCH 1/2] A net's layers include the layers it has pads on (#140) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UCbPYsA5LYNX8HZaGGZCpK --- README.md | 2 +- mcp/src/tools/link.js | 2 +- tests/browser/interaction.spec.js | 12 ++++++++++++ tests/browser/links.spec.js | 10 ++++++++++ tests/browser/smoke.spec.js | 12 ++++++++++++ web/app.js | 5 ++++- 6 files changed, 40 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fe63a64..069c1ab 100644 --- a/README.md +++ b/README.md @@ -296,7 +296,7 @@ The hash is a list of `key=value` pairs. Keys may repeat. | `zoom` | `board`, `selected` or `highlight`, same as W, E and Z. Used when there is no view. `selected` (and E) fits the highlighted nets as drawn, on layers that are on or that show them with Net, and also fits the markers. | | `copper` | Shown copper layer, as named in the CAD tool, e.g. `copper=F`. Layers not listed are off; `copper=` alone hides them all. Add `:` and letters to show only some kinds: `t` tracks, `z` zones, `v` vias, `s` silk, `f` fab. `copper=In1.Cu:tv` shows tracks and vias on In1.Cu. | | `netcopper` | Copper layer that is off but still shows the highlighted nets on it, e.g. `netcopper=In1.Cu`. Takes the same `:LETTERS` as `copper`, e.g. `netcopper=In1.Cu:t` for tracks only. May repeat. Read only together with `copper`; layers not listed show nothing while off. | -| `netlayers` | `1` turns on every layer the linked nets are routed on. | +| `netlayers` | `1` turns on every layer the linked nets have copper on: tracks, zones or pads. | Example: `board.html#net=/ADC1/CS&net=GND&netlayers=1&zoom=selected`. diff --git a/mcp/src/tools/link.js b/mcp/src/tools/link.js index 5b93fee..bb71418 100644 --- a/mcp/src/tools/link.js +++ b/mcp/src/tools/link.js @@ -71,7 +71,7 @@ export const linkInput = { ) .optional() .describe('Copper layers to show; the rest are hidden. [] hides them all. Leave out to keep the user\'s layers.'), - netlayers: z.boolean().optional().describe('Also show every layer the selected nets are routed on.'), + netlayers: z.boolean().optional().describe('Also show every layer the selected nets have tracks, zones or pads on.'), base_url: z .string() .optional() diff --git a/tests/browser/interaction.spec.js b/tests/browser/interaction.spec.js index 6544913..4278b76 100644 --- a/tests/browser/interaction.spec.js +++ b/tests/browser/interaction.spec.js @@ -933,6 +933,18 @@ test('T ticks Net on the layers the selected net is on, then unticks it', async for (const l of others) expect(s[l].net, l).toBe(false); }); +test('T ticks Net on a layer where the selected net has only pads', async ({ page }) => { + await load(page); + const onB = (s) => (fixture.pcbdata[s].B || []).map((t) => t.net); + const routedOnB = new Set([...onB('tracks'), ...onB('zones')]); + const net = footprints.flatMap((fp) => fp.pads) + .find((p) => p.net && p.layers.includes('B') && !routedOnB.has(p.net)).net; + await selectNetFromList(page, net); + await page.evaluate(() => document.activeElement.blur()); + await page.keyboard.press('t'); + expect((await state(page)).layers.B.net).toBe(true); +}); + // On a row that is off with Net ticked, tracks, zones and vias pick what is // drawn, so they are not dimmed. Silk stays dimmed. test('Net undims tracks, zones and vias on a row that is off', async ({ page }) => { diff --git a/tests/browser/links.spec.js b/tests/browser/links.spec.js index 29bea6f..3b4d048 100644 --- a/tests/browser/links.spec.js +++ b/tests/browser/links.spec.js @@ -309,6 +309,16 @@ test('netlayers=1 turns on the layers of the linked nets', async ({ page }) => { expect(s.layers[innerLayer].all).toBe(true); }); +test('netlayers=1 turns on a layer where a linked net has only pads', async ({ page }) => { + const onB = (s) => (fixture.pcbdata[s].B || []).map((t) => t.net); + const routedOnB = new Set([...onB('tracks'), ...onB('zones')]); + const net = footprints.flatMap((fp) => fp.pads) + .find((p) => p.net && p.layers.includes('B') && !routedOnB.has(p.net)).net; + await load(page, hashOf([['net', net], ['copper', 'F'], ['netlayers', '1']])); + const s = await state(page); + expect(s.layers.B.all).toBe(true); +}); + test('a shared link with layers restores the layer table', async ({ page, browser }) => { await load(page); await page.locator('#inner-layers-menu .menu-btn').hover(); diff --git a/tests/browser/smoke.spec.js b/tests/browser/smoke.spec.js index baaf438..c46b3ae 100644 --- a/tests/browser/smoke.spec.js +++ b/tests/browser/smoke.spec.js @@ -147,3 +147,15 @@ test('a net routed on an inner layer lists that layer', async ({ page }) => { ); expect(layers).toContain(layer); }); + +// A net with pads on B and no tracks or zones there still touches B. +test('a net with only pads on a layer lists that layer', async ({ page }) => { + await load(page); + const onB = (s) => (fixture.pcbdata[s].B || []).map((t) => t.net); + const routedOnB = new Set([...onB('tracks'), ...onB('zones')]); + const pad = fixture.pcbdata.footprints.flatMap((fp) => fp.pads) + .find((p) => p.net && p.layers.includes('B') && !routedOnB.has(p.net)); + expect(pad).toBeDefined(); + const layers = await page.evaluate((n) => window.__pcbaTest.netLayers(n), pad.net); + expect(layers).toContain('B'); +}); diff --git a/web/app.js b/web/app.js index ec56647..e1f4e21 100644 --- a/web/app.js +++ b/web/app.js @@ -52,6 +52,9 @@ function buildIndexes() { var net = fp.pads[j].net; if (!net) continue; componentToNets[i].add(net); + // A net touches every layer it has a pad on, even with no track there + if (!netToLayers[net]) netToLayers[net] = new Set(); + (fp.pads[j].layers || []).forEach(function(l) { netToLayers[net].add(l); }); if (!netToComponents[net]) netToComponents[net] = []; if (!seenNets.has(net)) { seenNets.add(net); @@ -59,7 +62,7 @@ function buildIndexes() { } } } - // Build netToLayers from tracks (skip vias) and zones + // Add the tracks (skip vias) and zones to netToLayers var _tl0 = performance.now(); if (pcbdata.tracks) { for (var layer in pcbdata.tracks) { From 62e24f7629b33b69195fa9f5cfccfe4a702dfcbe Mon Sep 17 00:00:00 2001 From: gherkin Date: Tue, 27 Oct 2026 21:18:48 +0100 Subject: [PATCH 2/2] Index pad layers with a plain loop; tests check the fixture pad exists Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UCbPYsA5LYNX8HZaGGZCpK --- tests/browser/interaction.spec.js | 7 ++++--- tests/browser/links.spec.js | 7 ++++--- web/app.js | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/browser/interaction.spec.js b/tests/browser/interaction.spec.js index 4278b76..a766e96 100644 --- a/tests/browser/interaction.spec.js +++ b/tests/browser/interaction.spec.js @@ -937,9 +937,10 @@ test('T ticks Net on a layer where the selected net has only pads', async ({ pag await load(page); const onB = (s) => (fixture.pcbdata[s].B || []).map((t) => t.net); const routedOnB = new Set([...onB('tracks'), ...onB('zones')]); - const net = footprints.flatMap((fp) => fp.pads) - .find((p) => p.net && p.layers.includes('B') && !routedOnB.has(p.net)).net; - await selectNetFromList(page, net); + const pad = footprints.flatMap((fp) => fp.pads) + .find((p) => p.net && p.layers.includes('B') && !routedOnB.has(p.net)); + expect(pad).toBeDefined(); + await selectNetFromList(page, pad.net); await page.evaluate(() => document.activeElement.blur()); await page.keyboard.press('t'); expect((await state(page)).layers.B.net).toBe(true); diff --git a/tests/browser/links.spec.js b/tests/browser/links.spec.js index 3b4d048..ddd4325 100644 --- a/tests/browser/links.spec.js +++ b/tests/browser/links.spec.js @@ -312,9 +312,10 @@ test('netlayers=1 turns on the layers of the linked nets', async ({ page }) => { test('netlayers=1 turns on a layer where a linked net has only pads', async ({ page }) => { const onB = (s) => (fixture.pcbdata[s].B || []).map((t) => t.net); const routedOnB = new Set([...onB('tracks'), ...onB('zones')]); - const net = footprints.flatMap((fp) => fp.pads) - .find((p) => p.net && p.layers.includes('B') && !routedOnB.has(p.net)).net; - await load(page, hashOf([['net', net], ['copper', 'F'], ['netlayers', '1']])); + const pad = footprints.flatMap((fp) => fp.pads) + .find((p) => p.net && p.layers.includes('B') && !routedOnB.has(p.net)); + expect(pad).toBeDefined(); + await load(page, hashOf([['net', pad.net], ['copper', 'F'], ['netlayers', '1']])); const s = await state(page); expect(s.layers.B.all).toBe(true); }); diff --git a/web/app.js b/web/app.js index e1f4e21..3a55b7c 100644 --- a/web/app.js +++ b/web/app.js @@ -54,7 +54,8 @@ function buildIndexes() { componentToNets[i].add(net); // A net touches every layer it has a pad on, even with no track there if (!netToLayers[net]) netToLayers[net] = new Set(); - (fp.pads[j].layers || []).forEach(function(l) { netToLayers[net].add(l); }); + var padLayers = fp.pads[j].layers || []; + for (var p = 0; p < padLayers.length; p++) netToLayers[net].add(padLayers[p]); if (!netToComponents[net]) netToComponents[net] = []; if (!seenNets.has(net)) { seenNets.add(net);