diff --git a/README.md b/README.md index 8394447..bebc18f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ and output an array of arrays for any issues found. | Function | Description | | --- | --- | | [`checkConnectorAccessibleOrientation`](./lib/check-connector-accessible-orientation.ts) | Returns `pcb_accessibility_error` for connectors whose orientation makes them inaccessible. | +| [`checkTestPointAccessibility`](./lib/check-testpoint-accessibility.ts) | Returns `pcb_placement_error` when a test point is inside another component's courtyard on the same PCB side. | | [`checkAllPinsInComponentAreUnderspecified`](./lib/check-all-pins-in-component-are-underspecified.ts) | Returns `source_component_pins_underspecified_warning` when every pin on a chip lacks pin attributes. | | [`checkNoPowerPinDefined`](./lib/check-no-power-pin-defined.ts) | Returns `source_no_power_pin_defined_warning` when a chip has no pin with `requires_power=true`. | | [`checkNoGroundPinDefined`](./lib/check-no-ground-pin-defined.ts) | Returns `source_no_ground_pin_defined_warning` when a chip has no pin with `requires_ground=true`. | @@ -31,7 +32,7 @@ and output an array of arrays for any issues found. | Function | Description | | --- | --- | -| [`runAllPlacementChecks`](./lib/run-all-checks.ts) | Runs placement checks (`checkViasOffBoard`, `checkPcbComponentsOutOfBoard`, `checkPcbComponentOverlap`, `checkPadPadClearance`, and `checkConnectorAccessibleOrientation`). | +| [`runAllPlacementChecks`](./lib/run-all-checks.ts) | Runs placement checks (`checkViasOffBoard`, `checkPcbComponentsOutOfBoard`, `checkPcbComponentOverlap`, `checkPadPadClearance`, `checkCourtyardOverlap`, `checkConnectorAccessibleOrientation`, and `checkTestPointAccessibility`). | | [`runAllNetlistChecks`](./lib/run-all-checks.ts) | Runs netlist connectivity checks (currently `checkPinMustBeConnected`). | | [`runAllPinSpecificationChecks`](./lib/run-all-checks.ts) | Runs pin specification checks (e.g. `checkAllPinsInComponentAreUnderspecified`, `checkNoPowerPinDefined`, and `checkNoGroundPinDefined`). | | [`runAllRoutingChecks`](./lib/run-all-checks.ts) | Runs all routing checks currently enabled (`checkEachPcbPortConnectedToPcbTraces`, `checkSourceTracesHavePcbTraces`, `checkEachPcbTraceNonOverlapping`, `checkPadTraceClearance`, `checkViaTraceClearance`, same/different net via spacing, and `checkPcbTracesOutOfBoard`). Trace-obstacle pairs are classified before aggregation, so each pair produces one overlap or clearance diagnostic, never both. | diff --git a/index.ts b/index.ts index 8330c1c..3c7d597 100644 --- a/index.ts +++ b/index.ts @@ -28,3 +28,4 @@ export { } from "./lib/run-all-checks" export { checkConnectorAccessibleOrientation } from "./lib/check-connector-accessible-orientation" +export { checkTestPointAccessibility } from "./lib/check-testpoint-accessibility" diff --git a/lib/check-testpoint-accessibility.ts b/lib/check-testpoint-accessibility.ts new file mode 100644 index 0000000..2399642 --- /dev/null +++ b/lib/check-testpoint-accessibility.ts @@ -0,0 +1,138 @@ +import { isPointInsidePolygon } from "@tscircuit/math-utils" +import type { + AnyCircuitElement, + PcbComponent, + PcbCourtyardCircle, + PcbCourtyardOutline, + PcbCourtyardPolygon, + PcbCourtyardRect, + PcbPlacementError, + SourceSimpleTestPoint, +} from "circuit-json" +import { getReadableNameForComponent } from "./util/get-readable-names" + +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 isPointInsideCourtyard = ( + point: { x: number; y: number }, + courtyard: CourtyardElement, +): boolean => { + if (courtyard.type === "pcb_courtyard_circle") { + const dx = point.x - courtyard.center.x + const dy = point.y - courtyard.center.y + return dx * dx + dy * dy <= courtyard.radius * courtyard.radius + } + + if (courtyard.type === "pcb_courtyard_rect") { + const angle = (-1 * (courtyard.ccw_rotation ?? 0) * Math.PI) / 180 + const dx = point.x - courtyard.center.x + const dy = point.y - courtyard.center.y + const localX = dx * Math.cos(angle) - dy * Math.sin(angle) + const localY = dx * Math.sin(angle) + dy * Math.cos(angle) + return ( + Math.abs(localX) <= courtyard.width / 2 && + Math.abs(localY) <= courtyard.height / 2 + ) + } + + const polygon = + courtyard.type === "pcb_courtyard_polygon" + ? courtyard.points + : courtyard.outline + return isPointInsidePolygon(point, polygon) +} + +const getPcbComponentName = ( + circuitJson: AnyCircuitElement[], + pcbComponentId: string, +): string => { + const pcbComponent = circuitJson.find( + (element): element is PcbComponent => + element.type === "pcb_component" && + element.pcb_component_id === pcbComponentId, + ) + const sourceComponent = circuitJson.find( + (element) => + element.type === "source_component" && + element.source_component_id === pcbComponent?.source_component_id, + ) + + return ( + (sourceComponent && "name" in sourceComponent + ? sourceComponent.name + : undefined) ?? getReadableNameForComponent(circuitJson, pcbComponentId) + ) +} + +/** + * Test points are intended to be contacted from their PCB side. A test point + * whose access center is covered by another component's courtyard cannot be + * reliably reached by a probe after assembly. + */ +export function checkTestPointAccessibility( + circuitJson: AnyCircuitElement[], +): PcbPlacementError[] { + const sourceTestPoints = circuitJson.filter( + (element): element is SourceSimpleTestPoint => + element.type === "source_component" && + element.ftype === "simple_test_point", + ) + const sourceTestPointIds = new Set( + sourceTestPoints.map((testPoint) => testPoint.source_component_id), + ) + const testPointNames = new Map( + sourceTestPoints.map((testPoint) => [ + testPoint.source_component_id, + testPoint.name, + ]), + ) + const testPointComponents = circuitJson.filter( + (element): element is PcbComponent => + element.type === "pcb_component" && + sourceTestPointIds.has(element.source_component_id), + ) + const courtyards = circuitJson.filter(isCourtyardElement) + const errors: PcbPlacementError[] = [] + const reportedComponentPairs = new Set() + + for (const testPoint of testPointComponents) { + for (const courtyard of courtyards) { + if (courtyard.pcb_component_id === testPoint.pcb_component_id) continue + if (courtyard.layer !== testPoint.layer) continue + if (!isPointInsideCourtyard(testPoint.center, courtyard)) continue + + const componentPair = `${testPoint.pcb_component_id}:${courtyard.pcb_component_id}` + if (reportedComponentPairs.has(componentPair)) continue + reportedComponentPairs.add(componentPair) + + const testPointName = + testPointNames.get(testPoint.source_component_id) ?? "Test point" + const obstructingComponentName = getPcbComponentName( + circuitJson, + courtyard.pcb_component_id, + ) + + errors.push({ + type: "pcb_placement_error", + pcb_placement_error_id: `testpoint_in_courtyard_${testPoint.pcb_component_id}_${courtyard.pcb_component_id}`, + error_type: "pcb_placement_error", + message: `Test point ${testPointName} is not accessible because it is inside the courtyard of ${obstructingComponentName}`, + subcircuit_id: testPoint.subcircuit_id, + }) + } + } + + return errors +} diff --git a/lib/run-all-checks.ts b/lib/run-all-checks.ts index b398c3e..184f908 100644 --- a/lib/run-all-checks.ts +++ b/lib/run-all-checks.ts @@ -18,6 +18,7 @@ import { checkSameNetViaSpacing } from "./check-same-net-via-spacing" import { checkSourceTracesHavePcbTraces } from "./check-source-traces-have-pcb-traces" import { checkPcbTracesOutOfBoard } from "./check-trace-out-of-board/checkTraceOutOfBoard" import { checkTracesAreContiguous } from "./check-traces-are-contiguous/check-traces-are-contiguous" +import { checkTestPointAccessibility } from "./check-testpoint-accessibility" import { checkViaTraceClearance } from "./check-via-trace-clearance" export async function runAllPlacementChecks(circuitJson: AnyCircuitElement[]) { @@ -28,6 +29,7 @@ export async function runAllPlacementChecks(circuitJson: AnyCircuitElement[]) { ...checkPadPadClearance(circuitJson), ...checkCourtyardOverlap(circuitJson), ...checkConnectorAccessibleOrientation(circuitJson), + ...checkTestPointAccessibility(circuitJson), ] } diff --git a/tests/lib/__snapshots__/check-testpoint-accessibility.snap.svg b/tests/lib/__snapshots__/check-testpoint-accessibility.snap.svg new file mode 100644 index 0000000..1478c78 --- /dev/null +++ b/tests/lib/__snapshots__/check-testpoint-accessibility.snap.svg @@ -0,0 +1 @@ +TP1 \ No newline at end of file diff --git a/tests/lib/check-testpoint-accessibility.test.tsx b/tests/lib/check-testpoint-accessibility.test.tsx new file mode 100644 index 0000000..1686f53 --- /dev/null +++ b/tests/lib/check-testpoint-accessibility.test.tsx @@ -0,0 +1,114 @@ +import { expect, test } from "bun:test" +import { convertCircuitJsonToPcbSvg } from "circuit-to-svg" +import { Circuit } from "tscircuit" +import { checkTestPointAccessibility } from "lib/check-testpoint-accessibility" +import { runAllPlacementChecks } from "lib/run-all-checks" + +const componentWithCourtyard = ( + + + + + } + pinLabels={{ 1: ["A"] }} + /> +) + +test("reports a test point inside another component's courtyard", async () => { + const circuit = new Circuit({ + platform: { placementDrcChecksDisabled: true }, + }) + circuit.add( + + {componentWithCourtyard} + + , + ) + + await circuit.renderUntilSettled() + const circuitJson = circuit.getCircuitJson() + const errors = checkTestPointAccessibility(circuitJson) + + expect(errors).toHaveLength(1) + expect(errors[0]).toMatchObject({ + type: "pcb_placement_error", + error_type: "pcb_placement_error", + message: + "Test point TP1 is not accessible because it is inside the courtyard of U1", + }) + expect( + convertCircuitJsonToPcbSvg([...circuitJson, ...errors], { + shouldDrawErrors: true, + showCourtyards: true, + }), + ).toMatchSvgSnapshot(import.meta.path) + + expect( + (await runAllPlacementChecks(circuitJson)).some( + (result) => result.message === errors[0].message, + ), + ).toBe(true) +}) + +test("allows a test point outside component courtyards", async () => { + const circuit = new Circuit({ + platform: { placementDrcChecksDisabled: true }, + }) + circuit.add( + + {componentWithCourtyard} + + , + ) + + await circuit.renderUntilSettled() + + expect(checkTestPointAccessibility(circuit.getCircuitJson())).toHaveLength(0) +}) + +test("allows a test point on the opposite PCB side", async () => { + const circuit = new Circuit({ + platform: { placementDrcChecksDisabled: true }, + }) + circuit.add( + + {componentWithCourtyard} + + , + ) + + await circuit.renderUntilSettled() + + expect(checkTestPointAccessibility(circuit.getCircuitJson())).toHaveLength(0) +})