diff --git a/lib/check-courtyard-overlap/checkCourtyardOverlap.ts b/lib/check-courtyard-overlap/checkCourtyardOverlap.ts index 5fff3d8..4107bb6 100644 --- a/lib/check-courtyard-overlap/checkCourtyardOverlap.ts +++ b/lib/check-courtyard-overlap/checkCourtyardOverlap.ts @@ -1,6 +1,6 @@ import { - areBoundsOverlappingPolygon, - getBoundsFromPoints, + doSegmentsIntersect, + isPointInsidePolygon, } from "@tscircuit/math-utils" import type { AnyCircuitElement, @@ -19,12 +19,19 @@ function getCourtyardPolygon(el: CourtyardElement): { x: number; y: number }[] { if (el.type === "pcb_courtyard_rect") { const hw = el.width / 2 const hh = el.height / 2 - return [ - { x: el.center.x - hw, y: el.center.y - hh }, - { x: el.center.x + hw, y: el.center.y - hh }, - { x: el.center.x + hw, y: el.center.y + hh }, - { x: el.center.x - hw, y: el.center.y + hh }, + const corners = [ + { x: -hw, y: -hh }, + { x: +hw, y: -hh }, + { x: +hw, y: +hh }, + { x: -hw, y: +hh }, ] + const angle = ((el.ccw_rotation ?? 0) * Math.PI) / 180 + const cos = Math.cos(angle) + const sin = Math.sin(angle) + return corners.map(({ x, y }) => ({ + x: el.center.x + x * cos - y * sin, + y: el.center.y + x * sin + y * cos, + })) } if (el.type === "pcb_courtyard_circle") { const N = 32 @@ -59,6 +66,25 @@ function getComponentName( return pcbComponentId } +type Point = { x: number; y: number } + +function polygonsOverlap(polyA: Point[], polyB: Point[]): boolean { + // Check if any vertex of A is inside B or vice versa + if (polyA.some((p) => isPointInsidePolygon(p, polyB))) return true + if (polyB.some((p) => isPointInsidePolygon(p, polyA))) return true + // Check if any edge of A intersects any edge of B + for (let i = 0; i < polyA.length; i++) { + const a1 = polyA[i] + const a2 = polyA[(i + 1) % polyA.length] + for (let j = 0; j < polyB.length; j++) { + const b1 = polyB[j] + const b2 = polyB[(j + 1) % polyB.length] + if (doSegmentsIntersect(a1, a2, b1, b2)) return true + } + } + return false +} + /** * Check for overlapping PCB component courtyards. * Returns one error per pair of components whose courtyard elements overlap. @@ -94,13 +120,7 @@ export function checkCourtyardOverlap( for (const b of byComponent.get(idB)!) { const polyA = getCourtyardPolygon(a) const polyB = getCourtyardPolygon(b) - const boundsA = getBoundsFromPoints(polyA) - const boundsB = getBoundsFromPoints(polyB) - if (!boundsA || !boundsB) continue - if ( - areBoundsOverlappingPolygon(boundsA, polyB) || - areBoundsOverlappingPolygon(boundsB, polyA) - ) { + if (polygonsOverlap(polyA, polyB)) { overlapping = true break outer } diff --git a/package.json b/package.json index 2f31bde..2a4455f 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,10 @@ "@types/bun": "^1.2.8", "@types/debug": "^4.1.12", "bun-match-svg": "^0.0.11", - "circuit-to-svg": "^0.0.334", - "circuit-json": "^0.0.400", + "circuit-to-svg": "^0.0.337", + "circuit-json": "^0.0.403", "debug": "^4.3.5", - "tscircuit": "^0.0.1487", + "tscircuit": "^0.0.1515", "zod": "^3.23.8", "tsup": "^8.2.3" }, diff --git a/tests/lib/__snapshots__/check-connector-accessible-orientation.snap.svg b/tests/lib/__snapshots__/check-connector-accessible-orientation.snap.svg index 1148055..63aa444 100644 --- a/tests/lib/__snapshots__/check-connector-accessible-orientation.snap.svg +++ b/tests/lib/__snapshots__/check-connector-accessible-orientation.snap.svg @@ -1 +1 @@ -Component J1 extends outside board boundaries by 2.87mm. Try moving it 2.87mm right to fit within the board edge. \ No newline at end of file +Component J1 extends outside board boundaries by 2.87mm. Try moving it 2.87mm right to fit within the board edge. \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-circle-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-circle-overlap.snap.svg index 0c2663f..5c1092e 100644 --- a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-circle-overlap.snap.svg +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-circle-overlap.snap.svg @@ -1 +1 @@ -Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file +Courtyard of U1 overlaps with courtyard of U2Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-circle-vs-rotated-rect-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-circle-vs-rotated-rect-overlap.snap.svg new file mode 100644 index 0000000..c90c364 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-circle-vs-rotated-rect-overlap.snap.svg @@ -0,0 +1 @@ +Courtyard of U1 overlaps with courtyard of U2Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-mixed-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-mixed-overlap.snap.svg index 5e7a753..9b8cb07 100644 --- a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-mixed-overlap.snap.svg +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-mixed-overlap.snap.svg @@ -1 +1 @@ -Courtyard of U1 overlaps with courtyard of C1Courtyard of U1 overlaps with courtyard of R1 \ No newline at end of file +Courtyard of U1 overlaps with courtyard of C1Courtyard of U1 overlaps with courtyard of R1Courtyard of U1 overlaps with courtyard of C1Courtyard of U1 overlaps with courtyard of R1 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-overlap-no-pad-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-overlap-no-pad-overlap.snap.svg index e6acfbd..2997929 100644 --- a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-overlap-no-pad-overlap.snap.svg +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-overlap-no-pad-overlap.snap.svg @@ -1 +1 @@ -Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file +Courtyard of U1 overlaps with courtyard of U2Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-polygon-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-polygon-overlap.snap.svg index 126749d..d23964d 100644 --- a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-polygon-overlap.snap.svg +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-polygon-overlap.snap.svg @@ -1 +1 @@ -Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file +Courtyard of U1 overlaps with courtyard of U2Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-vs-rotated-rect-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-vs-rotated-rect-overlap.snap.svg new file mode 100644 index 0000000..42120c8 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-outline-vs-rotated-rect-overlap.snap.svg @@ -0,0 +1 @@ +Courtyard of U1 overlaps with courtyard of U2Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-0-vs-45-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-0-vs-45-overlap.snap.svg new file mode 100644 index 0000000..d547454 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-0-vs-45-overlap.snap.svg @@ -0,0 +1 @@ +Courtyard of U1 overlaps with courtyard of U2Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-0-vs-90-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-0-vs-90-overlap.snap.svg new file mode 100644 index 0000000..98de462 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-0-vs-90-overlap.snap.svg @@ -0,0 +1 @@ +Courtyard of U1 overlaps with courtyard of U2Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-30-vs-60-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-30-vs-60-overlap.snap.svg new file mode 100644 index 0000000..15cb414 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-30-vs-60-overlap.snap.svg @@ -0,0 +1 @@ +Courtyard of U1 overlaps with courtyard of U2Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-overlap.snap.svg b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-overlap.snap.svg index 6c7420a..86f18b3 100644 --- a/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-overlap.snap.svg +++ b/tests/lib/check-pcb-component-overlap/__snapshots__/courtyard-rect-overlap.snap.svg @@ -1 +1 @@ -Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file +Courtyard of U1 overlaps with courtyard of U2Courtyard of U1 overlaps with courtyard of U2 \ No newline at end of file diff --git a/tests/lib/check-pcb-component-overlap/courtyard-circle-vs-rotated-rect-overlap.test.tsx b/tests/lib/check-pcb-component-overlap/courtyard-circle-vs-rotated-rect-overlap.test.tsx new file mode 100644 index 0000000..546bdcb --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/courtyard-circle-vs-rotated-rect-overlap.test.tsx @@ -0,0 +1,87 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { Circuit } from "tscircuit" +import { checkCourtyardOverlap } from "lib/check-courtyard-overlap/checkCourtyardOverlap" + +/** + * U1 at (0,0): circle courtyard radius=2mm + * U2 at (2,0) rotated 45°: rect courtyard 4×1mm + * U2's corner at (~0.23, -1.06) is inside the circle (distance ≈ 1.09 < 2) + * + * Expected: 1 overlap error (U1–U2) + */ + +test("courtyard overlap: circle vs rotated rect", async () => { + const circuit = new Circuit() + circuit.add( + + + + + + + } + /> + + + + + + } + /> + , + ) + await circuit.renderUntilSettled() + const circuitJson = circuit.getCircuitJson() + + const errors = checkCourtyardOverlap(circuitJson) + expect(errors).toHaveLength(1) + expect(errors[0].type).toBe("pcb_courtyard_overlap_error") + expect(errors[0].pcb_component_ids).toHaveLength(2) + + expect( + convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { + shouldDrawErrors: true, + showCourtyards: true, + }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/lib/check-pcb-component-overlap/courtyard-outline-vs-rotated-rect-overlap.test.tsx b/tests/lib/check-pcb-component-overlap/courtyard-outline-vs-rotated-rect-overlap.test.tsx new file mode 100644 index 0000000..ec57e35 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/courtyard-outline-vs-rotated-rect-overlap.test.tsx @@ -0,0 +1,95 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { Circuit } from "tscircuit" +import { checkCourtyardOverlap } from "lib/check-courtyard-overlap/checkCourtyardOverlap" + +/** + * U1 at (0,0): outline courtyard 2.4×1.4mm → x∈[-1.2,1.2], y∈[-0.7,0.7] + * (matches pads at ±0.5mm with ~0.3mm courtyard margin) + * U2 at (1.5,0) rotated 45°: rect courtyard 4×1mm + * U2's long edge crosses U1's bottom outline edge at x≈0.33 + * + * Expected: 1 overlap error (U1–U2) + */ + +const COURTYARD_OUTLINE = [ + { x: -1.2, y: -0.7 }, + { x: 1.2, y: -0.7 }, + { x: 1.2, y: 0.7 }, + { x: -1.2, y: 0.7 }, +] + +test("courtyard overlap: outline vs rotated rect", async () => { + const circuit = new Circuit() + circuit.add( + + + + + + + } + /> + + + + + + } + /> + , + ) + await circuit.renderUntilSettled() + const circuitJson = circuit.getCircuitJson() + + const errors = checkCourtyardOverlap(circuitJson) + expect(errors).toHaveLength(1) + expect(errors[0].type).toBe("pcb_courtyard_overlap_error") + expect(errors[0].pcb_component_ids).toHaveLength(2) + + expect( + convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { + shouldDrawErrors: true, + showCourtyards: true, + }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/lib/check-pcb-component-overlap/courtyard-rect-0-vs-45-overlap.test.tsx b/tests/lib/check-pcb-component-overlap/courtyard-rect-0-vs-45-overlap.test.tsx new file mode 100644 index 0000000..b7df770 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/courtyard-rect-0-vs-45-overlap.test.tsx @@ -0,0 +1,65 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { Circuit } from "tscircuit" +import { checkCourtyardOverlap } from "lib/check-courtyard-overlap/checkCourtyardOverlap" + +/** + * U1 at (0,0) 0°: 4×1mm courtyard → x∈[-2,2], y∈[-0.5,0.5] + * U2 at (1.5,0) 45°: diagonal edge crosses into U1 + * + * Expected: 1 overlap error (U1–U2) + */ + +const Chip = (props: { name: string; pcbX: number; pcbRotation?: number }) => ( + + + + + + } + /> +) + +test("courtyard overlap: 0° vs 45° rotation", async () => { + const circuit = new Circuit() + circuit.add( + + + + , + ) + await circuit.renderUntilSettled() + const circuitJson = circuit.getCircuitJson() + + const errors = checkCourtyardOverlap(circuitJson) + expect(errors).toHaveLength(1) + expect(errors[0].type).toBe("pcb_courtyard_overlap_error") + expect(errors[0].pcb_component_ids).toHaveLength(2) + + expect( + convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { + shouldDrawErrors: true, + showCourtyards: true, + }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/lib/check-pcb-component-overlap/courtyard-rect-0-vs-90-overlap.test.tsx b/tests/lib/check-pcb-component-overlap/courtyard-rect-0-vs-90-overlap.test.tsx new file mode 100644 index 0000000..00d58e0 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/courtyard-rect-0-vs-90-overlap.test.tsx @@ -0,0 +1,66 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { Circuit } from "tscircuit" +import { checkCourtyardOverlap } from "lib/check-courtyard-overlap/checkCourtyardOverlap" + +/** + * U1 at (0,0) 0°: 4×1mm courtyard → x∈[-2,2], y∈[-0.5,0.5] + * U2 at (2.2,0) 90°: 4×1mm rotated 90° → x∈[1.7,2.7], y∈[-2,2] + * U1's corner (2,-0.5) falls inside U2 + * + * Expected: 1 overlap error (U1–U2) + */ + +const Chip = (props: { name: string; pcbX: number; pcbRotation?: number }) => ( + + + + + + } + /> +) + +test("courtyard overlap: 0° vs 90° rotation", async () => { + const circuit = new Circuit() + circuit.add( + + + + , + ) + await circuit.renderUntilSettled() + const circuitJson = circuit.getCircuitJson() + + const errors = checkCourtyardOverlap(circuitJson) + expect(errors).toHaveLength(1) + expect(errors[0].type).toBe("pcb_courtyard_overlap_error") + expect(errors[0].pcb_component_ids).toHaveLength(2) + + expect( + convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { + shouldDrawErrors: true, + showCourtyards: true, + }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/lib/check-pcb-component-overlap/courtyard-rect-30-vs-60-overlap.test.tsx b/tests/lib/check-pcb-component-overlap/courtyard-rect-30-vs-60-overlap.test.tsx new file mode 100644 index 0000000..8c29972 --- /dev/null +++ b/tests/lib/check-pcb-component-overlap/courtyard-rect-30-vs-60-overlap.test.tsx @@ -0,0 +1,66 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { Circuit } from "tscircuit" +import { checkCourtyardOverlap } from "lib/check-courtyard-overlap/checkCourtyardOverlap" + +/** + * U1 at (0,0) 30°: 4×1mm rotated 30° → rightmost corner at x≈1.98 + * U2 at (2,0) 60°: 4×1mm rotated 60° → leftmost corner at x≈0.57 + * U1's rightmost corner falls inside U2's polygon + * + * Expected: 1 overlap error (U1–U2) + */ + +const Chip = (props: { name: string; pcbX: number; pcbRotation?: number }) => ( + + + + + + } + /> +) + +test("courtyard overlap: 30° vs 60° rotation", async () => { + const circuit = new Circuit() + circuit.add( + + + + , + ) + await circuit.renderUntilSettled() + const circuitJson = circuit.getCircuitJson() + + const errors = checkCourtyardOverlap(circuitJson) + expect(errors).toHaveLength(1) + expect(errors[0].type).toBe("pcb_courtyard_overlap_error") + expect(errors[0].pcb_component_ids).toHaveLength(2) + + expect( + convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { + shouldDrawErrors: true, + showCourtyards: true, + }), + ).toMatchSvgSnapshot(import.meta.path) +}) diff --git a/tests/lib/pcb-component-boundary/__snapshots__/false-positive-component-outside.snap.svg b/tests/lib/pcb-component-boundary/__snapshots__/false-positive-component-outside.snap.svg index 0095108..dcd34aa 100644 --- a/tests/lib/pcb-component-boundary/__snapshots__/false-positive-component-outside.snap.svg +++ b/tests/lib/pcb-component-boundary/__snapshots__/false-positive-component-outside.snap.svg @@ -1 +1 @@ -VBUSGND3V3GP0GP1GP2U2SWDRUNGP16GP17GP18GP19U3Component U2 extends outside board boundaries by 4.07mm. Try moving it 4.07mm right to fit within the board edge.Component U3 extends outside board boundaries by 4.07mm. Try moving it 4.07mm left to fit within the board edge.Component U2 extends outside board boundaries by 4.07mm. Try moving it 4.07mm right to fit within the board edge.Component U3 extends outside board boundaries by 4.07mm. Try moving it 4.07mm left to fit within the board edge. \ No newline at end of file +VBUSGND3V3GP0GP1GP2U2SWDRUNGP16GP17GP18GP19U3Component U2 extends outside board boundaries by 4.07mm. Try moving it 4.07mm right to fit within the board edge.Component U3 extends outside board boundaries by 4.07mm. Try moving it 4.07mm left to fit within the board edge.Component U2 extends outside board boundaries by 4.07mm. Try moving it 4.07mm right to fit within the board edge.Component U3 extends outside board boundaries by 4.07mm. Try moving it 4.07mm left to fit within the board edge. \ No newline at end of file diff --git a/tests/repros/__snapshots__/repro01.snap.svg b/tests/repros/__snapshots__/repro01.snap.svg index 6722697..64aaee4 100644 --- a/tests/repros/__snapshots__/repro01.snap.svg +++ b/tests/repros/__snapshots__/repro01.snap.svg @@ -1 +1 @@ -pcb_smtpad USBC1.A4B9 overlaps with pcb_smtpad U1.USB_DP \ No newline at end of file +pcb_smtpad USBC1.A4B9 overlaps with pcb_smtpad U1.USB_DP \ No newline at end of file