diff --git a/index.ts b/index.ts index 69ff30f..8330c1c 100644 --- a/index.ts +++ b/index.ts @@ -10,6 +10,7 @@ export { checkSourceTracesHavePcbTraces } from "./lib/check-source-traces-have-p export { checkTracesAreContiguous } from "./lib/check-traces-are-contiguous/check-traces-are-contiguous" export { checkPcbTracesOutOfBoard } from "./lib/check-trace-out-of-board/checkTraceOutOfBoard" export { checkPcbComponentOverlap } from "./lib/check-pcb-components-overlap/checkPcbComponentOverlap" +export { checkPcbTraceLengths } from "./lib/check-pcb-trace-lengths" export { checkPadPadClearance } from "./lib/check-pad-pad-clearance" export { checkPadTraceClearance } from "./lib/check-pad-trace-clearance" export { checkViaTraceClearance } from "./lib/check-via-trace-clearance" diff --git a/lib/check-pcb-trace-lengths.ts b/lib/check-pcb-trace-lengths.ts new file mode 100644 index 0000000..addc3df --- /dev/null +++ b/lib/check-pcb-trace-lengths.ts @@ -0,0 +1,93 @@ +import type { + AnyCircuitElement, + PcbTrace, + PcbTraceRoutePoint, + PcbTraceTooLongWarning, + SourceTrace, +} from "circuit-json" + +const DEFAULT_VIA_LENGTH_MM = 1.6 + +const getRoutePointPosition = (routePoint: PcbTraceRoutePoint) => + routePoint.route_type === "through_pad" + ? routePoint.start + : { x: routePoint.x, y: routePoint.y } + +const getPcbTraceLength = (pcbTrace: PcbTrace): number => { + if (pcbTrace.trace_length !== undefined) return pcbTrace.trace_length + + let traceLength = 0 + + for ( + let routePointIndex = 0; + routePointIndex < pcbTrace.route.length; + routePointIndex++ + ) { + const routePoint = pcbTrace.route[routePointIndex] + if (!routePoint) continue + + if (routePoint.route_type === "via") { + traceLength += DEFAULT_VIA_LENGTH_MM + continue + } + + const nextRoutePoint = pcbTrace.route[routePointIndex + 1] + if (!nextRoutePoint) continue + + const routePointPosition = getRoutePointPosition(routePoint) + const nextRoutePointPosition = getRoutePointPosition(nextRoutePoint) + traceLength += Math.hypot( + nextRoutePointPosition.x - routePointPosition.x, + nextRoutePointPosition.y - routePointPosition.y, + ) + } + + return traceLength +} + +export const checkPcbTraceLengths = ( + circuitJson: AnyCircuitElement[], +): PcbTraceTooLongWarning[] => { + const sourceTraces = circuitJson.filter( + (element): element is SourceTrace => element.type === "source_trace", + ) + const pcbTraces = circuitJson.filter( + (element): element is PcbTrace => element.type === "pcb_trace", + ) + + const sourceTracesById = new Map( + sourceTraces.map((sourceTrace) => [ + sourceTrace.source_trace_id, + sourceTrace, + ]), + ) + const warnings: PcbTraceTooLongWarning[] = [] + + for (const pcbTrace of pcbTraces) { + if (!pcbTrace.source_trace_id) continue + + const sourceTrace = sourceTracesById.get(pcbTrace.source_trace_id) + if (!sourceTrace) continue + + const maximumTraceLength = sourceTrace.max_length + if (typeof maximumTraceLength !== "number") continue + + const actualTraceLength = getPcbTraceLength(pcbTrace) + if (actualTraceLength <= maximumTraceLength) continue + + warnings.push({ + type: "pcb_trace_too_long_warning", + pcb_trace_too_long_warning_id: `pcb_trace_too_long_warning_${pcbTrace.pcb_trace_id}`, + warning_type: "pcb_trace_too_long_warning", + message: `PCB trace is ${actualTraceLength.toFixed(2)}mm long, exceeding the ${maximumTraceLength}mm maximum`, + pcb_trace_id: pcbTrace.pcb_trace_id, + source_trace_id: sourceTrace.source_trace_id, + source_net_id: sourceTrace.connected_source_net_ids[0], + actual_trace_length: actualTraceLength, + maximum_trace_length: maximumTraceLength, + subcircuit_id: pcbTrace.subcircuit_id ?? sourceTrace.subcircuit_id, + }) + } + + return warnings +} diff --git a/lib/run-all-checks.ts b/lib/run-all-checks.ts index fbdf0c1..b398c3e 100644 --- a/lib/run-all-checks.ts +++ b/lib/run-all-checks.ts @@ -12,6 +12,7 @@ import { checkPadTraceClearance } from "./check-pad-trace-clearance" import { checkPcbComponentsOutOfBoard } from "./check-pcb-components-out-of-board/checkPcbComponentsOutOfBoard" import { checkViasOffBoard } from "./check-pcb-components-out-of-board/checkViasOffBoard" import { checkPcbComponentOverlap } from "./check-pcb-components-overlap/checkPcbComponentOverlap" +import { checkPcbTraceLengths } from "./check-pcb-trace-lengths" import { checkPinMustBeConnected } from "./check-pin-must-be-connected" import { checkSameNetViaSpacing } from "./check-same-net-via-spacing" import { checkSourceTracesHavePcbTraces } from "./check-source-traces-have-pcb-traces" @@ -48,6 +49,7 @@ export async function runAllRoutingChecks(circuitJson: AnyCircuitElement[]) { return [ ...checkEachPcbPortConnectedToPcbTraces(circuitJson), ...checkSourceTracesHavePcbTraces(circuitJson), + ...checkPcbTraceLengths(circuitJson), ...checkEachPcbTraceNonOverlapping(circuitJson), ...checkPadTraceClearance(circuitJson), ...checkViaTraceClearance(circuitJson), diff --git a/package.json b/package.json index ab8e583..330573f 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,10 @@ "@types/bun": "^1.2.8", "@types/debug": "^4.1.12", "bun-match-svg": "^0.0.11", - "circuit-json": "^0.0.428", + "circuit-json": "^0.0.453", "circuit-to-svg": "^0.0.388", "debug": "^4.3.5", + "format-si-unit": "^0.0.7", "tscircuit": "^0.0.1786", "tsup": "^8.2.3", "zod": "^3.23.8" diff --git a/tests/lib/check-pcb-trace-lengths.test.ts b/tests/lib/check-pcb-trace-lengths.test.ts new file mode 100644 index 0000000..c14ee65 --- /dev/null +++ b/tests/lib/check-pcb-trace-lengths.test.ts @@ -0,0 +1,78 @@ +import { expect, test } from "bun:test" +import type { AnyCircuitElement } from "circuit-json" +import { checkPcbTraceLengths, runAllRoutingChecks } from "../.." + +const circuitJson = [ + { + type: "source_trace", + source_trace_id: "overlength_source_trace", + connected_source_port_ids: [], + connected_source_net_ids: ["signal_net"], + max_length: 10, + subcircuit_id: "board", + }, + { + type: "source_trace", + source_trace_id: "short_source_trace", + connected_source_port_ids: [], + connected_source_net_ids: [], + max_length: 10, + }, + { + type: "source_trace", + source_trace_id: "unconstrained_source_trace", + connected_source_port_ids: [], + connected_source_net_ids: [], + max_length: null, + }, + { + type: "pcb_trace", + pcb_trace_id: "overlength_pcb_trace", + source_trace_id: "overlength_source_trace", + subcircuit_id: "board", + route: [ + { route_type: "wire", x: 0, y: 0, width: 0.15, layer: "top" }, + { route_type: "wire", x: 12, y: 0, width: 0.15, layer: "top" }, + ], + }, + { + type: "pcb_trace", + pcb_trace_id: "short_pcb_trace", + source_trace_id: "short_source_trace", + trace_length: 8, + route: [], + }, + { + type: "pcb_trace", + pcb_trace_id: "unconstrained_pcb_trace", + source_trace_id: "unconstrained_source_trace", + trace_length: 20, + route: [], + }, +] as AnyCircuitElement[] + +test("warns when a PCB trace exceeds its source trace maximum length", () => { + expect(checkPcbTraceLengths(circuitJson)).toEqual([ + { + type: "pcb_trace_too_long_warning", + pcb_trace_too_long_warning_id: + "pcb_trace_too_long_warning_overlength_pcb_trace", + warning_type: "pcb_trace_too_long_warning", + message: "PCB trace is 12.00mm long, exceeding the 10mm maximum", + pcb_trace_id: "overlength_pcb_trace", + source_trace_id: "overlength_source_trace", + source_net_id: "signal_net", + actual_trace_length: 12, + maximum_trace_length: 10, + subcircuit_id: "board", + }, + ]) +}) + +test("is included in the routing check pipeline", async () => { + const results = await runAllRoutingChecks(circuitJson) + + expect( + results.filter((result) => result.type === "pcb_trace_too_long_warning"), + ).toEqual(checkPcbTraceLengths(circuitJson)) +})