From 5831e275d8565ec2f41d2a82816ad8f6d8c85b66 Mon Sep 17 00:00:00 2001 From: abdalrouf-AAA <236259618+technologyet31-create@users.noreply.github.com> Date: Sun, 5 Jul 2026 13:49:15 +0200 Subject: [PATCH 1/2] fix: require a trace endpoint's pad to be on the same layer checkTracesAreContiguous matched an endpoint to a pad by x/y only, so a top-layer wire endpoint counted as connected to a bottom-layer pad. Gate the match on layer: isPointInPad now takes the endpoint's layer as a separate param and rejects a pad on a different layer (smt pad -> its layer, plated hole -> its copper layers). Flips the repro tests from #165 to passing and updates the shouldDrawErrors snapshot to show the now-flagged endpoint. Co-authored-by: abdalrouf-AAA <236259618+technologyet31-create@users.noreply.github.com> --- .../check-traces-are-contiguous.ts | 26 +++++++++++++++---- .../is-point-in-pad.ts | 11 ++++++++ ...trace-endpoint-on-different-layer.snap.svg | 2 +- tests/lib/check-traces-are-contiguous.test.ts | 4 +-- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/check-traces-are-contiguous/check-traces-are-contiguous.ts b/lib/check-traces-are-contiguous/check-traces-are-contiguous.ts index b75e32f..3d5fcc5 100644 --- a/lib/check-traces-are-contiguous/check-traces-are-contiguous.ts +++ b/lib/check-traces-are-contiguous/check-traces-are-contiguous.ts @@ -52,7 +52,7 @@ function routePointConnectsToAnotherExpectedPort( const expectedPads = padMap.get(expectedPort.pcb_port_id) return ( expectedPads?.some((pad) => - isPointInPad({ x: point.x, y: point.y }, pad), + isPointInPad({ x: point.x, y: point.y }, pad, point.layer), ) ?? false ) }) @@ -273,13 +273,21 @@ function checkTracesAreContiguous( const isFirstPointConnected = firstPoint.route_type === "wire" && pads.some((pad) => - isPointInPad({ x: firstPoint.x, y: firstPoint.y }, pad), + isPointInPad( + { x: firstPoint.x, y: firstPoint.y }, + pad, + firstPoint.layer, + ), ) const isLastPointConnected = lastPoint.route_type === "wire" && pads.some((pad) => - isPointInPad({ x: lastPoint.x, y: lastPoint.y }, pad), + isPointInPad( + { x: lastPoint.x, y: lastPoint.y }, + pad, + lastPoint.layer, + ), ) if (!isFirstPointConnected && !isLastPointConnected) { @@ -326,7 +334,11 @@ function checkTracesAreContiguous( if ( firstPoint.route_type === "wire" && pads.some((pad) => - isPointInPad({ x: firstPoint.x, y: firstPoint.y }, pad), + isPointInPad( + { x: firstPoint.x, y: firstPoint.y }, + pad, + firstPoint.layer, + ), ) ) { firstConnectsToAnyPad = true @@ -334,7 +346,11 @@ function checkTracesAreContiguous( if ( lastPoint.route_type === "wire" && pads.some((pad) => - isPointInPad({ x: lastPoint.x, y: lastPoint.y }, pad), + isPointInPad( + { x: lastPoint.x, y: lastPoint.y }, + pad, + lastPoint.layer, + ), ) ) { lastConnectsToAnyPad = true diff --git a/lib/check-traces-are-contiguous/is-point-in-pad.ts b/lib/check-traces-are-contiguous/is-point-in-pad.ts index e786e83..4ec84c3 100644 --- a/lib/check-traces-are-contiguous/is-point-in-pad.ts +++ b/lib/check-traces-are-contiguous/is-point-in-pad.ts @@ -34,7 +34,18 @@ function isPointOnSegment( export function isPointInPad( point: { x: number; y: number }, pad: PcbSmtPad | PcbPlatedHole, + layer?: string, ): boolean { + // A trace endpoint only connects to a pad on the same layer (a top-layer + // wire can't land on a bottom-layer smt pad without a via). + if (layer !== undefined) { + const padIsOnLayer = + pad.type === "pcb_smtpad" + ? pad.layer === layer + : pad.layers.some((padLayer) => padLayer === layer) + if (!padIsOnLayer) return false + } + if (pad.type === "pcb_smtpad") { if (pad.shape === "circle") { return getDistanceBetweenPoints(point, pad) <= pad.radius diff --git a/tests/lib/__snapshots__/trace-endpoint-on-different-layer.snap.svg b/tests/lib/__snapshots__/trace-endpoint-on-different-layer.snap.svg index 41cbce8..baf4829 100644 --- a/tests/lib/__snapshots__/trace-endpoint-on-different-layer.snap.svg +++ b/tests/lib/__snapshots__/trace-endpoint-on-different-layer.snap.svg @@ -1 +1 @@ - \ No newline at end of file +Trace [trace[t1]] is missing a connection to smtpad[#pp2] \ No newline at end of file diff --git a/tests/lib/check-traces-are-contiguous.test.ts b/tests/lib/check-traces-are-contiguous.test.ts index 6af8b7e..539426f 100644 --- a/tests/lib/check-traces-are-contiguous.test.ts +++ b/tests/lib/check-traces-are-contiguous.test.ts @@ -410,7 +410,7 @@ test("still reports a source-trace branch that does not reach a required port", ) }) -test.failing( +test( "errors when a trace endpoint touches a pad on a different layer", () => { // A top-layer wire endpoint at (1,0) coincides with a BOTTOM-layer pad. @@ -475,7 +475,7 @@ test.failing( }, ) -test.failing( +test( "draws an error where a trace endpoint touches a pad on a different layer", () => { // Same scenario rendered with shouldDrawErrors: a top-layer wire ends on From 5462ba11944b7cbe37b662f24fc87a44b3963d5f Mon Sep 17 00:00:00 2001 From: abdalrouf-AAA <236259618+technologyet31-create@users.noreply.github.com> Date: Sat, 1 Aug 2026 20:33:38 +0200 Subject: [PATCH 2/2] Run biome format on the trace-layer test Co-authored-by: abdalrouf-AAA <236259618+technologyet31-create@users.noreply.github.com> --- tests/lib/check-traces-are-contiguous.test.ts | 278 +++++++++--------- 1 file changed, 136 insertions(+), 142 deletions(-) diff --git a/tests/lib/check-traces-are-contiguous.test.ts b/tests/lib/check-traces-are-contiguous.test.ts index 539426f..4901ac3 100644 --- a/tests/lib/check-traces-are-contiguous.test.ts +++ b/tests/lib/check-traces-are-contiguous.test.ts @@ -410,148 +410,142 @@ test("still reports a source-trace branch that does not reach a required port", ) }) -test( - "errors when a trace endpoint touches a pad on a different layer", - () => { - // A top-layer wire endpoint at (1,0) coincides with a BOTTOM-layer pad. - // They cannot connect without a via, so this should be flagged. - const circuitJson = [ - { - type: "source_trace", - source_trace_id: "trace_1", - connected_source_port_ids: ["port_1", "port_2"], - }, - { - type: "pcb_port", - pcb_port_id: "pp1", - source_port_id: "port_1", - x: 0, - y: 0, - layers: ["top"], - }, - { - type: "pcb_port", - pcb_port_id: "pp2", - source_port_id: "port_2", - x: 1, - y: 0, - layers: ["top"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad_1", - pcb_port_id: "pp1", - shape: "rect", - x: 0, - y: 0, - width: 0.5, - height: 0.5, - layer: "top", - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad_2", - pcb_port_id: "pp2", - shape: "rect", - x: 1, - y: 0, - width: 0.5, - height: 0.5, - layer: "bottom", - }, - { - type: "pcb_trace", - pcb_trace_id: "t1", - source_trace_id: "trace_1", - route: [ - { route_type: "wire", x: 0, y: 0, layer: "top", width: 0.2 }, - { route_type: "wire", x: 1, y: 0, layer: "top", width: 0.2 }, - ], - }, - ] as AnyCircuitElement[] +test("errors when a trace endpoint touches a pad on a different layer", () => { + // A top-layer wire endpoint at (1,0) coincides with a BOTTOM-layer pad. + // They cannot connect without a via, so this should be flagged. + const circuitJson = [ + { + type: "source_trace", + source_trace_id: "trace_1", + connected_source_port_ids: ["port_1", "port_2"], + }, + { + type: "pcb_port", + pcb_port_id: "pp1", + source_port_id: "port_1", + x: 0, + y: 0, + layers: ["top"], + }, + { + type: "pcb_port", + pcb_port_id: "pp2", + source_port_id: "port_2", + x: 1, + y: 0, + layers: ["top"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad_1", + pcb_port_id: "pp1", + shape: "rect", + x: 0, + y: 0, + width: 0.5, + height: 0.5, + layer: "top", + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad_2", + pcb_port_id: "pp2", + shape: "rect", + x: 1, + y: 0, + width: 0.5, + height: 0.5, + layer: "bottom", + }, + { + type: "pcb_trace", + pcb_trace_id: "t1", + source_trace_id: "trace_1", + route: [ + { route_type: "wire", x: 0, y: 0, layer: "top", width: 0.2 }, + { route_type: "wire", x: 1, y: 0, layer: "top", width: 0.2 }, + ], + }, + ] as AnyCircuitElement[] - const errors = checkTracesAreContiguous(circuitJson) - expect(errors.length).toBeGreaterThan(0) - }, -) + const errors = checkTracesAreContiguous(circuitJson) + expect(errors.length).toBeGreaterThan(0) +}) -test( - "draws an error where a trace endpoint touches a pad on a different layer", - () => { - // Same scenario rendered with shouldDrawErrors: a top-layer wire ends on - // the BOTTOM-layer pad on the right. On unfixed code the check misses it, - // so no error is drawn; the fix makes the error marker appear. - const circuitJson = [ - { - type: "pcb_board", - pcb_board_id: "board_1", - center: { x: 0.5, y: 0 }, - width: 3, - height: 2, - thickness: 1.6, - num_layers: 2, - material: "fr4", - }, - { - type: "source_trace", - source_trace_id: "trace_1", - connected_source_port_ids: ["port_1", "port_2"], - }, - { - type: "pcb_port", - pcb_port_id: "pp1", - source_port_id: "port_1", - x: 0, - y: 0, - layers: ["top"], - }, - { - type: "pcb_port", - pcb_port_id: "pp2", - source_port_id: "port_2", - x: 1, - y: 0, - layers: ["top"], - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad_1", - pcb_port_id: "pp1", - shape: "rect", - x: 0, - y: 0, - width: 0.5, - height: 0.5, - layer: "top", - }, - { - type: "pcb_smtpad", - pcb_smtpad_id: "pad_2", - pcb_port_id: "pp2", - shape: "rect", - x: 1, - y: 0, - width: 0.5, - height: 0.5, - layer: "bottom", - }, - { - type: "pcb_trace", - pcb_trace_id: "t1", - source_trace_id: "trace_1", - route: [ - { route_type: "wire", x: 0, y: 0, layer: "top", width: 0.2 }, - { route_type: "wire", x: 1, y: 0, layer: "top", width: 0.2 }, - ], - }, - ] as AnyCircuitElement[] +test("draws an error where a trace endpoint touches a pad on a different layer", () => { + // Same scenario rendered with shouldDrawErrors: a top-layer wire ends on + // the BOTTOM-layer pad on the right. On unfixed code the check misses it, + // so no error is drawn; the fix makes the error marker appear. + const circuitJson = [ + { + type: "pcb_board", + pcb_board_id: "board_1", + center: { x: 0.5, y: 0 }, + width: 3, + height: 2, + thickness: 1.6, + num_layers: 2, + material: "fr4", + }, + { + type: "source_trace", + source_trace_id: "trace_1", + connected_source_port_ids: ["port_1", "port_2"], + }, + { + type: "pcb_port", + pcb_port_id: "pp1", + source_port_id: "port_1", + x: 0, + y: 0, + layers: ["top"], + }, + { + type: "pcb_port", + pcb_port_id: "pp2", + source_port_id: "port_2", + x: 1, + y: 0, + layers: ["top"], + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad_1", + pcb_port_id: "pp1", + shape: "rect", + x: 0, + y: 0, + width: 0.5, + height: 0.5, + layer: "top", + }, + { + type: "pcb_smtpad", + pcb_smtpad_id: "pad_2", + pcb_port_id: "pp2", + shape: "rect", + x: 1, + y: 0, + width: 0.5, + height: 0.5, + layer: "bottom", + }, + { + type: "pcb_trace", + pcb_trace_id: "t1", + source_trace_id: "trace_1", + route: [ + { route_type: "wire", x: 0, y: 0, layer: "top", width: 0.2 }, + { route_type: "wire", x: 1, y: 0, layer: "top", width: 0.2 }, + ], + }, + ] as AnyCircuitElement[] - const errors = checkTracesAreContiguous(circuitJson) - expect( - convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { - shouldDrawErrors: true, - }), - ).toMatchSvgSnapshot(import.meta.path, "trace-endpoint-on-different-layer") - expect(errors.length).toBeGreaterThan(0) - }, -) + const errors = checkTracesAreContiguous(circuitJson) + expect( + convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { + shouldDrawErrors: true, + }), + ).toMatchSvgSnapshot(import.meta.path, "trace-endpoint-on-different-layer") + expect(errors.length).toBeGreaterThan(0) +})