Add DRC check: vias too close to pads#127
Open
blessuselessk wants to merge 2 commits intotscircuit:mainfrom
Open
Add DRC check: vias too close to pads#127blessuselessk wants to merge 2 commits intotscircuit:mainfrom
blessuselessk wants to merge 2 commits intotscircuit:mainfrom
Conversation
Adds checkViaToPadSpacing that detects vias placed too close to SMT pads using rect-to-circle distance calculations. Handles all pad shapes: rect, circle, pill, rotated_rect, rotated_pill, and polygon (bbox fallback). Closes tscircuit#44
Comment on lines
+6
to
+272
| test("returns error when via is too close to a rect pad on same layer", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_smtpad", | ||
| pcb_smtpad_id: "pad1", | ||
| shape: "rect", | ||
| x: 0, | ||
| y: 0, | ||
| width: 1.0, | ||
| height: 0.5, | ||
| layer: "top", | ||
| }, | ||
| { | ||
| type: "pcb_via", | ||
| pcb_via_id: "via1", | ||
| x: 0.7, | ||
| y: 0, | ||
| hole_diameter: 0.3, | ||
| outer_diameter: 0.6, | ||
| layers: ["top", "bottom"], | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| // Pad right edge at x=0.5, via left edge at x=0.7-0.3=0.4 → gap = -0.1 (overlapping) | ||
| const errors = checkViaToPadSpacing(soup) | ||
| expect(errors).toHaveLength(1) | ||
| expect(errors[0].message).toContain("too close to pad") | ||
| }) | ||
|
|
||
| test("no error when via is far enough from rect pad", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_smtpad", | ||
| pcb_smtpad_id: "pad1", | ||
| shape: "rect", | ||
| x: 0, | ||
| y: 0, | ||
| width: 1.0, | ||
| height: 0.5, | ||
| layer: "top", | ||
| }, | ||
| { | ||
| type: "pcb_via", | ||
| pcb_via_id: "via1", | ||
| x: 1.2, | ||
| y: 0, | ||
| hole_diameter: 0.3, | ||
| outer_diameter: 0.6, | ||
| layers: ["top", "bottom"], | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| // Pad right edge at x=0.5, via left edge at x=1.2-0.3=0.9 → gap = 0.4 | ||
| const errors = checkViaToPadSpacing(soup) | ||
| expect(errors).toHaveLength(0) | ||
| }) | ||
|
|
||
| test("no error when via and pad are on different layers", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_smtpad", | ||
| pcb_smtpad_id: "pad1", | ||
| shape: "rect", | ||
| x: 0, | ||
| y: 0, | ||
| width: 1.0, | ||
| height: 0.5, | ||
| layer: "bottom", | ||
| }, | ||
| { | ||
| type: "pcb_via", | ||
| pcb_via_id: "via1", | ||
| x: 0.5, | ||
| y: 0, | ||
| hole_diameter: 0.3, | ||
| outer_diameter: 0.6, | ||
| layers: ["top"], | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| const errors = checkViaToPadSpacing(soup) | ||
| expect(errors).toHaveLength(0) | ||
| }) | ||
|
|
||
| test("returns error when via is too close to a circle pad", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_smtpad", | ||
| pcb_smtpad_id: "pad1", | ||
| shape: "circle", | ||
| x: 0, | ||
| y: 0, | ||
| radius: 0.5, | ||
| layer: "top", | ||
| }, | ||
| { | ||
| type: "pcb_via", | ||
| pcb_via_id: "via1", | ||
| x: 0.8, | ||
| y: 0, | ||
| hole_diameter: 0.3, | ||
| outer_diameter: 0.6, | ||
| layers: ["top", "bottom"], | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| // Center-to-center: 0.8, pad radius: 0.5, via radius: 0.3 → gap = 0.0 < 0.2 | ||
| const errors = checkViaToPadSpacing(soup) | ||
| expect(errors).toHaveLength(1) | ||
| expect(errors[0].message).toContain("too close to pad") | ||
| }) | ||
|
|
||
| test("no error when via is far enough from circle pad", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_smtpad", | ||
| pcb_smtpad_id: "pad1", | ||
| shape: "circle", | ||
| x: 0, | ||
| y: 0, | ||
| radius: 0.5, | ||
| layer: "top", | ||
| }, | ||
| { | ||
| type: "pcb_via", | ||
| pcb_via_id: "via1", | ||
| x: 1.2, | ||
| y: 0, | ||
| hole_diameter: 0.3, | ||
| outer_diameter: 0.6, | ||
| layers: ["top", "bottom"], | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| // Center-to-center: 1.2, pad radius: 0.5, via radius: 0.3 → gap = 0.4 | ||
| const errors = checkViaToPadSpacing(soup) | ||
| expect(errors).toHaveLength(0) | ||
| }) | ||
|
|
||
| test("handles pill-shaped pads correctly", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_smtpad", | ||
| pcb_smtpad_id: "pad1", | ||
| shape: "pill", | ||
| x: 0, | ||
| y: 0, | ||
| width: 2.0, | ||
| height: 1.0, | ||
| radius: 0.5, | ||
| layer: "top", | ||
| }, | ||
| { | ||
| type: "pcb_via", | ||
| pcb_via_id: "via1", | ||
| // Place just outside the pill's right semicircle end | ||
| x: 1.3, | ||
| y: 0, | ||
| hole_diameter: 0.3, | ||
| outer_diameter: 0.6, | ||
| layers: ["top", "bottom"], | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| // Inner rect: width=1.0, height=0.0 (pill radius=0.5, height=1.0 → inner height=0) | ||
| // Effective radius = via_radius(0.3) + pill_radius(0.5) = 0.8 | ||
| // Nearest point on inner rect at origin to via center (1.3, 0): distance from (0.5, 0) to (1.3, 0) = 0.8 | ||
| // gap = 0.8 - 0.8 = 0.0 < 0.2 | ||
| const errors = checkViaToPadSpacing(soup) | ||
| expect(errors).toHaveLength(1) | ||
| }) | ||
|
|
||
| test("returns no errors when there are no vias", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_smtpad", | ||
| pcb_smtpad_id: "pad1", | ||
| shape: "rect", | ||
| x: 0, | ||
| y: 0, | ||
| width: 1.0, | ||
| height: 0.5, | ||
| layer: "top", | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| const errors = checkViaToPadSpacing(soup) | ||
| expect(errors).toHaveLength(0) | ||
| }) | ||
|
|
||
| test("returns no errors when there are no pads", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_via", | ||
| pcb_via_id: "via1", | ||
| x: 0, | ||
| y: 0, | ||
| hole_diameter: 0.3, | ||
| outer_diameter: 0.6, | ||
| layers: ["top", "bottom"], | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| const errors = checkViaToPadSpacing(soup) | ||
| expect(errors).toHaveLength(0) | ||
| }) | ||
|
|
||
| test("respects custom minSpacing parameter", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_smtpad", | ||
| pcb_smtpad_id: "pad1", | ||
| shape: "rect", | ||
| x: 0, | ||
| y: 0, | ||
| width: 1.0, | ||
| height: 0.5, | ||
| layer: "top", | ||
| }, | ||
| { | ||
| type: "pcb_via", | ||
| pcb_via_id: "via1", | ||
| x: 1.0, | ||
| y: 0, | ||
| hole_diameter: 0.3, | ||
| outer_diameter: 0.6, | ||
| layers: ["top", "bottom"], | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| // Gap: nearest point on rect (0.5, 0) to via center (1.0, 0) = 0.5, minus via radius 0.3 = 0.2 | ||
| // With default minSpacing=0.2, passes (gap == minSpacing) | ||
| expect(checkViaToPadSpacing(soup)).toHaveLength(0) | ||
|
|
||
| // With larger minSpacing=0.3, fails | ||
| expect(checkViaToPadSpacing(soup, { minSpacing: 0.3 })).toHaveLength(1) | ||
| }) | ||
|
|
||
| test("detects via too close to pad diagonally", () => { | ||
| const soup: AnyCircuitElement[] = [ | ||
| { | ||
| type: "pcb_smtpad", | ||
| pcb_smtpad_id: "pad1", | ||
| shape: "rect", | ||
| x: 0, | ||
| y: 0, | ||
| width: 1.0, | ||
| height: 1.0, | ||
| layer: "top", | ||
| }, | ||
| { | ||
| type: "pcb_via", | ||
| pcb_via_id: "via1", | ||
| // Near the corner of the pad | ||
| x: 0.7, | ||
| y: 0.7, | ||
| hole_diameter: 0.3, | ||
| outer_diameter: 0.6, | ||
| layers: ["top", "bottom"], | ||
| }, | ||
| ] as AnyCircuitElement[] | ||
|
|
||
| // Nearest rect point to (0.7, 0.7) is (0.5, 0.5), distance = sqrt(0.04+0.04) ≈ 0.283 | ||
| // gap = 0.283 - 0.3 ≈ -0.017 (overlapping) | ||
| const errors = checkViaToPadSpacing(soup) | ||
| expect(errors).toHaveLength(1) | ||
| }) |
Contributor
There was a problem hiding this comment.
This test file contains multiple test() calls (at least 10 different test cases), which violates the rule that a *.test.ts file may have AT MOST one test(...). After that, the user should split into multiple, numbered files. This file should be split into multiple numbered files like check-via-to-pad-spacing1.test.ts, check-via-to-pad-spacing2.test.ts, etc., with each file containing only one test() call.
Spotted by Graphite (based on custom rule: Custom rule)
Is this helpful? React 👍 or 👎 to let us know.
PcbSmtPadPolygon lacks x/y properties, causing TS2339 errors on build and type-check. Added getPadCenter() helper that computes bounding-box centroid for polygon pads and returns x/y directly for all other shapes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
checkViaToPadSpacing— a new DRC check that detects vias placed too close to SMT padsPcbSmtPadshapes:rect,circle,pill,rotated_rect,rotated_pill,polygon(bounding-box fallback)DEFAULT_VIA_TO_PAD_MARGIN), configurable viaminSpacingrunAllRoutingChecksand exported from the package indexTest plan
minSpacingCloses #44