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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ No newline at end of file