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..a766e96 100644 --- a/tests/browser/interaction.spec.js +++ b/tests/browser/interaction.spec.js @@ -933,6 +933,19 @@ 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 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); +}); + // 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..ddd4325 100644 --- a/tests/browser/links.spec.js +++ b/tests/browser/links.spec.js @@ -309,6 +309,17 @@ 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 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); +}); + 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..3a55b7c 100644 --- a/web/app.js +++ b/web/app.js @@ -52,6 +52,10 @@ 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(); + 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); @@ -59,7 +63,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) {