From 05aaa1eac2d6fda9d65144e409167aaf3feffaa4 Mon Sep 17 00:00:00 2001 From: mohan-bee Date: Wed, 13 May 2026 15:21:36 +0530 Subject: [PATCH 1/4] Detect footprint overlap when holes collides with courtyards --- .../checkPcbComponentOverlap.ts | 44 ++++++++++++- .../doPcbElementsOverlap.ts | 28 ++++++++- ...le-overlapping-resistor-courtyard.snap.svg | 1 + ...le-overlapping-resistor-courtyard.test.tsx | 61 +++++++++++++++++++ 4 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 tests/lib/check-pcb-component-overlap/__snapshots__/hole-overlapping-resistor-courtyard.snap.svg create mode 100644 tests/lib/check-pcb-component-overlap/hole-overlapping-resistor-courtyard.test.tsx diff --git a/lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts b/lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts index 104bf4a..b5a506f 100644 --- a/lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts +++ b/lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts @@ -4,7 +4,14 @@ import { getPrimaryId, } from "@tscircuit/circuit-json-util" import { doBoundsOverlap } from "@tscircuit/math-utils" -import type { AnyCircuitElement, PcbFootprintOverlapError } from "circuit-json" +import type { + AnyCircuitElement, + PcbCourtyardCircle, + PcbCourtyardOutline, + PcbCourtyardPolygon, + PcbCourtyardRect, + PcbFootprintOverlapError, +} from "circuit-json" import { getFullConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map" import { getReadableNameForElementId, @@ -26,6 +33,20 @@ interface ComponentWithElements { } } +type CourtyardElement = + | PcbCourtyardCircle + | PcbCourtyardOutline + | PcbCourtyardPolygon + | PcbCourtyardRect + +const isCourtyardElement = ( + element: AnyCircuitElement, +): element is CourtyardElement => + element.type === "pcb_courtyard_circle" || + element.type === "pcb_courtyard_outline" || + element.type === "pcb_courtyard_polygon" || + element.type === "pcb_courtyard_rect" + const formatOverlapElementDescription = ( circuitJson: AnyCircuitElement[], element: OverlappableElement, @@ -55,6 +76,7 @@ export function checkPcbComponentOverlap( const smtPads = cju(circuitJson).pcb_smtpad.list() const platedHoles = cju(circuitJson).pcb_plated_hole.list() const holes = cju(circuitJson).pcb_hole.list() + const courtyards = circuitJson.filter(isCourtyardElement) // Group elements by component (or treat standalone elements as their own "component") const componentMap = new Map() @@ -97,6 +119,18 @@ export function checkPcbComponentOverlap( }) } + for (const courtyard of courtyards) { + const componentId = courtyard.pcb_component_id + if (!componentMap.has(componentId)) { + componentMap.set(componentId, { + component_id: componentId, + elements: [], + bounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 }, + }) + } + componentMap.get(componentId)!.elements.push(courtyard) + } + // Compute bounds for each component for (const [componentId, componentData] of componentMap) { if (componentData.elements.length > 0) { @@ -124,6 +158,14 @@ export function checkPcbComponentOverlap( const id1 = getPrimaryId(elem1) const id2 = getPrimaryId(elem2) + if ( + (isCourtyardElement(elem1) || isCourtyardElement(elem2)) && + elem1.type !== "pcb_hole" && + elem2.type !== "pcb_hole" + ) { + continue + } + // Check if both are SMT pads and are electrically connected (same net) - if so, skip // This allows pads with the same subcircuit connectivity to be in contact if ( diff --git a/lib/check-pcb-components-overlap/doPcbElementsOverlap.ts b/lib/check-pcb-components-overlap/doPcbElementsOverlap.ts index 612820f..771b45e 100644 --- a/lib/check-pcb-components-overlap/doPcbElementsOverlap.ts +++ b/lib/check-pcb-components-overlap/doPcbElementsOverlap.ts @@ -1,12 +1,36 @@ import { getBoundsOfPcbElements } from "@tscircuit/circuit-json-util" import { doBoundsOverlap } from "@tscircuit/math-utils" -import type { PcbHole, PcbPlatedHole, PcbSmtPad } from "circuit-json" +import type { + PcbCourtyardCircle, + PcbCourtyardOutline, + PcbCourtyardPolygon, + PcbCourtyardRect, + PcbHole, + PcbPlatedHole, + PcbSmtPad, +} from "circuit-json" import type { Collidable } from "lib/check-each-pcb-trace-non-overlapping/getCollidableBounds" import { getLayersOfPcbElement } from "lib/util/getLayersOfPcbElement" -export type OverlappableElement = PcbSmtPad | PcbPlatedHole | PcbHole +export type OverlappableElement = + | PcbSmtPad + | PcbPlatedHole + | PcbHole + | PcbCourtyardCircle + | PcbCourtyardOutline + | PcbCourtyardPolygon + | PcbCourtyardRect function getElementLayers(elem: OverlappableElement): string[] { + if ( + elem.type === "pcb_courtyard_circle" || + elem.type === "pcb_courtyard_outline" || + elem.type === "pcb_courtyard_polygon" || + elem.type === "pcb_courtyard_rect" + ) { + return [elem.layer] + } + return getLayersOfPcbElement(elem as Collidable) } diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/hole-overlapping-resistor-courtyard.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/hole-overlapping-resistor-courtyard.snap.svg new file mode 100644 index 0000000..206b2d0 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/hole-overlapping-resistor-courtyard.snap.svg @@ -0,0 +1 @@ +R1pcb_hole [pcb_hole_0] overlaps with pcb_courtyard_outline [pcb_courtyard_outline_0] \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/hole-overlapping-resistor-courtyard.test.tsx b/tests/lib/check-pcb-component-overlap/hole-overlapping-resistor-courtyard.test.tsx new file mode 100644 index 0000000..d7d688f --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/hole-overlapping-resistor-courtyard.test.tsx @@ -0,0 +1,61 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { Circuit } from "tscircuit" +import { checkPcbComponentOverlap } from "lib/check-pcb-components-overlap/checkPcbComponentOverlap" + +test("hole overlapping resistor courtyard should show footprint overlap error", async () => { + const circuit = new Circuit() + + circuit.add( + + + + + + + } + /> + + , + ) + + await circuit.renderUntilSettled() + const circuitJson = circuit.getCircuitJson() + const errors = checkPcbComponentOverlap(circuitJson) + const messages = errors.map((error) => error.message) + + expect( + errors.every((error) => error.type === "pcb_footprint_overlap_error"), + ).toBe(true) + expect( + errors.every((error) => error.pcb_hole_ids?.includes("pcb_hole_0")), + ).toBe(true) + expect( + messages.some((message) => message.includes("pcb_courtyard_outline")), + ).toBe(true) + + expect( + convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { + shouldDrawErrors: true, + showCourtyards: true, + }), + ).toMatchSvgSnapshot(import.meta.path) +}) From 6c8d9141df0554e05ba7b4ad379e70c83f69a72d Mon Sep 17 00:00:00 2001 From: mohan-bee Date: Wed, 13 May 2026 15:24:05 +0530 Subject: [PATCH 2/4] up --- ...le-overlapping-resistor-courtyard.test.tsx | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/tests/lib/check-pcb-component-overlap/hole-overlapping-resistor-courtyard.test.tsx b/tests/lib/check-pcb-component-overlap/hole-overlapping-resistor-courtyard.test.tsx index d7d688f..1795702 100644 --- a/tests/lib/check-pcb-component-overlap/hole-overlapping-resistor-courtyard.test.tsx +++ b/tests/lib/check-pcb-component-overlap/hole-overlapping-resistor-courtyard.test.tsx @@ -7,34 +7,28 @@ test("hole overlapping resistor courtyard should show footprint overlap error", const circuit = new Circuit() circuit.add( - - - - - - - } - /> - - , + + + + + + + } + /> + , ) await circuit.renderUntilSettled() From e09c7b1705718ec7d272e9d9bdff235274a20c3d Mon Sep 17 00:00:00 2001 From: mohan-bee Date: Wed, 13 May 2026 22:50:27 +0530 Subject: [PATCH 3/4] up --- .../checkPcbComponentOverlap.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts b/lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts index b5a506f..7179525 100644 --- a/lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts +++ b/lib/check-pcb-components-overlap/checkPcbComponentOverlap.ts @@ -25,7 +25,7 @@ import { interface ComponentWithElements { component_id: string elements: OverlappableElement[] - bounds: { + bounds?: { minX: number minY: number maxX: number @@ -89,7 +89,6 @@ export function checkPcbComponentOverlap( componentMap.set(componentId, { component_id: componentId, elements: [], - bounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 }, }) } componentMap.get(componentId)!.elements.push(pad) @@ -103,7 +102,6 @@ export function checkPcbComponentOverlap( componentMap.set(componentId, { component_id: componentId, elements: [], - bounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 }, }) } componentMap.get(componentId)!.elements.push(hole) @@ -115,7 +113,6 @@ export function checkPcbComponentOverlap( componentMap.set(componentId, { component_id: componentId, elements: [hole], - bounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 }, }) } @@ -125,7 +122,6 @@ export function checkPcbComponentOverlap( componentMap.set(componentId, { component_id: componentId, elements: [], - bounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 }, }) } componentMap.get(componentId)!.elements.push(courtyard) @@ -148,6 +144,10 @@ export function checkPcbComponentOverlap( const comp2 = componentsWithElements[j] // First check if component bounds overlap + if (!comp1.bounds || !comp2.bounds) { + continue + } + if (!doBoundsOverlap(comp1.bounds, comp2.bounds)) { continue } From 399b48bb56ea37f3ea22de298e9c856116fee0cd Mon Sep 17 00:00:00 2001 From: mohan-bee Date: Fri, 15 May 2026 12:39:17 +0530 Subject: [PATCH 4/4] repro for mounting holes inside own courtyard --- tests/repros/__snapshots__/repro04.snap.svg | 1 + tests/repros/repro04.test.tsx | 256 ++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 tests/repros/__snapshots__/repro04.snap.svg create mode 100644 tests/repros/repro04.test.tsx diff --git a/tests/repros/__snapshots__/repro04.snap.svg b/tests/repros/__snapshots__/repro04.snap.svg new file mode 100644 index 0000000..5e7ff05 --- /dev/null +++ b/tests/repros/__snapshots__/repro04.snap.svg @@ -0,0 +1 @@ +J1pcb_courtyard_outline [pcb_courtyard_outline_0] overlaps with pcb_hole [pcb_hole_0]pcb_courtyard_outline [pcb_courtyard_outline_0] overlaps with pcb_hole [pcb_hole_1] \ No newline at end of file diff --git a/tests/repros/repro04.test.tsx b/tests/repros/repro04.test.tsx new file mode 100644 index 0000000..78e29a1 --- /dev/null +++ b/tests/repros/repro04.test.tsx @@ -0,0 +1,256 @@ +import { expect, test } from "bun:test" +import type { ChipProps } from "@tscircuit/props" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { Circuit } from "tscircuit" +import { runAllPlacementChecks } from "lib/run-all-checks" + +const pinLabels = { + pin13: ["EH1"], + pin14: ["EH2"], + pin15: ["pin13_alt1"], + pin16: ["pin14_alt1"], + pin17: ["A1B12"], + pin18: ["A4B9"], + pin19: ["B8"], + pin20: ["A5"], + pin21: ["B7"], + pin22: ["A6"], + pin23: ["A7"], + pin24: ["B6"], + pin25: ["A8"], + pin26: ["B5"], + pin27: ["B4A9"], + pin28: ["B1A12"], +} as const + +const TYPE_C_16PIN_2MD_073_ = (props: ChipProps) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + } + cadModel={{ + objUrl: + "https://modelcdn.tscircuit.com/easyeda_models/assets/C2765186.obj?uuid=4ee8413127e64716b804db03d4b340ae", + stepUrl: + "https://modelcdn.tscircuit.com/easyeda_models/assets/C2765186.step?uuid=4ee8413127e64716b804db03d4b340ae", + pcbRotationOffset: 0, + modelOriginPosition: { x: 0, y: 1.3249976000000152, z: -1.6800018 }, + }} + {...props} + /> + ) +} + +test("type-c footprint mounting holes inside own courtyard do not cause placement errors", async () => { + const circuit = new Circuit() + + circuit.add( + + + , + ) + + await circuit.renderUntilSettled() + + const circuitJson = circuit.getCircuitJson() + const placementErrors = await runAllPlacementChecks(circuitJson) + // expect(placementErrors).toHaveLength(0) + + expect( + convertCircuitJsonToPcbSvg([...circuitJson, ...placementErrors], { + shouldDrawErrors: true, + showCourtyards: true, + }), + ).toMatchSvgSnapshot(import.meta.path) +})