From f1dc1faf84bfdd6dd3efe5f46059c5301a738539 Mon Sep 17 00:00:00 2001 From: seveibar Date: Wed, 22 Jul 2026 22:18:43 -0700 Subject: [PATCH 1/2] Check crystal trace lengths across routed nets --- index.ts | 1 + lib/check-pcb-trace-lengths.ts | 166 +++++++++++++++++ lib/run-all-checks.ts | 2 + package.json | 3 +- tests/lib/check-pcb-trace-lengths.test.ts | 217 ++++++++++++++++++++++ 5 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 lib/check-pcb-trace-lengths.ts create mode 100644 tests/lib/check-pcb-trace-lengths.test.ts 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..71feb15 --- /dev/null +++ b/lib/check-pcb-trace-lengths.ts @@ -0,0 +1,166 @@ +import type { + AnyCircuitElement, + PcbTrace, + PcbTraceRoutePoint, + PcbTraceTooLongWarning, + SourcePort, + SourceTrace, +} from "circuit-json" + +const DEFAULT_CRYSTAL_MAX_TRACE_LENGTH_MM = 10 +const DEFAULT_VIA_LENGTH_MM = 1.6 + +type SubcircuitConnectivityMapKey = NonNullable< + SourceTrace["subcircuit_connectivity_map_key"] +> + +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 crystalSourceComponentIds = new Set( + circuitJson + .filter( + (element) => + element.type === "source_component" && + element.ftype === "simple_crystal", + ) + .map((sourceComponent) => sourceComponent.source_component_id), + ) + const sourcePorts = circuitJson.filter( + (element): element is SourcePort => element.type === "source_port", + ) + 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 sourcePortsById = new Map( + sourcePorts.map((sourcePort) => [sourcePort.source_port_id, sourcePort]), + ) + const sourceTracesById = new Map( + sourceTraces.map((sourceTrace) => [ + sourceTrace.source_trace_id, + sourceTrace, + ]), + ) + const crystalMaxLengthsBySubcircuitConnectivityMapKey = new Map< + SubcircuitConnectivityMapKey, + number + >() + const crystalMaxLengthsBySourceTraceId = new Map() + + for (const sourceTrace of sourceTraces) { + const isConnectedToCrystal = sourceTrace.connected_source_port_ids.some( + (sourcePortId) => { + const sourcePort = sourcePortsById.get(sourcePortId) + if (!sourcePort?.source_component_id) return false + + return crystalSourceComponentIds.has(sourcePort.source_component_id) + }, + ) + if (!isConnectedToCrystal) continue + + const crystalMaxLength = + sourceTrace.max_length ?? DEFAULT_CRYSTAL_MAX_TRACE_LENGTH_MM + crystalMaxLengthsBySourceTraceId.set( + sourceTrace.source_trace_id, + crystalMaxLength, + ) + + const subcircuitConnectivityMapKey = + sourceTrace.subcircuit_connectivity_map_key + if (!subcircuitConnectivityMapKey) continue + + const existingCrystalMaxLength = + crystalMaxLengthsBySubcircuitConnectivityMapKey.get( + subcircuitConnectivityMapKey, + ) + + crystalMaxLengthsBySubcircuitConnectivityMapKey.set( + subcircuitConnectivityMapKey, + existingCrystalMaxLength === undefined + ? crystalMaxLength + : Math.min(existingCrystalMaxLength, crystalMaxLength), + ) + } + + 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 propagatedCrystalMaxLength = + sourceTrace.subcircuit_connectivity_map_key + ? crystalMaxLengthsBySubcircuitConnectivityMapKey.get( + sourceTrace.subcircuit_connectivity_map_key, + ) + : undefined + const maximumTraceLengths = [ + sourceTrace.max_length, + crystalMaxLengthsBySourceTraceId.get(sourceTrace.source_trace_id), + propagatedCrystalMaxLength, + ].filter((length): length is number => typeof length === "number") + if (maximumTraceLengths.length === 0) continue + + const maximumTraceLength = Math.min(...maximumTraceLengths) + 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..cc91675 --- /dev/null +++ b/tests/lib/check-pcb-trace-lengths.test.ts @@ -0,0 +1,217 @@ +import { expect, test } from "bun:test" +import type { AnyCircuitElement } from "circuit-json" +import { checkPcbTraceLengths, runAllRoutingChecks } from "../.." + +const circuitJson = [ + { + type: "source_component", + source_component_id: "crystal", + ftype: "simple_crystal", + name: "Y1", + frequency: 12_000_000, + }, + { + type: "source_component", + source_component_id: "mcu", + ftype: "simple_chip", + name: "U1", + }, + { + type: "source_component", + source_component_id: "load_capacitor", + ftype: "simple_capacitor", + name: "C1", + capacitance: 10e-12, + }, + { + type: "source_component", + source_component_id: "unrelated_resistor", + ftype: "simple_resistor", + name: "R1", + resistance: 1_000, + }, + { + type: "source_port", + source_port_id: "crystal_port", + source_component_id: "crystal", + name: "pin1", + pin_number: 1, + port_hints: ["1"], + }, + { + type: "source_port", + source_port_id: "mcu_port", + source_component_id: "mcu", + name: "XTAL_IN", + pin_number: 1, + port_hints: ["XTAL_IN"], + }, + { + type: "source_port", + source_port_id: "capacitor_port", + source_component_id: "load_capacitor", + name: "pin1", + pin_number: 1, + port_hints: ["1"], + }, + { + type: "source_port", + source_port_id: "resistor_port", + source_component_id: "unrelated_resistor", + name: "pin1", + pin_number: 1, + port_hints: ["1"], + }, + { + type: "source_trace", + source_trace_id: "crystal_trace", + connected_source_port_ids: ["crystal_port"], + connected_source_net_ids: ["crystal_net"], + subcircuit_connectivity_map_key: "board_crystal_net", + }, + { + type: "source_trace", + source_trace_id: "mcu_trace", + connected_source_port_ids: ["mcu_port"], + connected_source_net_ids: ["crystal_net"], + subcircuit_connectivity_map_key: "board_crystal_net", + }, + { + type: "source_trace", + source_trace_id: "capacitor_trace", + connected_source_port_ids: ["capacitor_port"], + connected_source_net_ids: ["crystal_net"], + subcircuit_connectivity_map_key: "board_crystal_net", + }, + { + type: "source_trace", + source_trace_id: "unrelated_trace", + connected_source_port_ids: ["resistor_port"], + connected_source_net_ids: ["unrelated_net"], + subcircuit_connectivity_map_key: "board_unrelated_net", + }, + { + type: "pcb_trace", + pcb_trace_id: "pcb_trace_to_mcu", + source_trace_id: "mcu_trace", + 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: "pcb_trace_to_capacitor", + source_trace_id: "capacitor_trace", + route: [ + { route_type: "wire", x: 0, y: 0, width: 0.15, layer: "top" }, + { route_type: "wire", x: 0, y: 11, width: 0.15, layer: "top" }, + ], + }, + { + type: "pcb_trace", + pcb_trace_id: "pcb_trace_unrelated", + source_trace_id: "unrelated_trace", + route: [ + { route_type: "wire", x: 0, y: 0, width: 0.15, layer: "top" }, + { route_type: "wire", x: 20, y: 0, width: 0.15, layer: "top" }, + ], + }, +] as AnyCircuitElement[] + +test("warns for MCU and capacitor traces on a crystal net", () => { + const warnings = checkPcbTraceLengths(circuitJson) + + expect( + warnings.map((warning) => ({ + pcb_trace_id: warning.pcb_trace_id, + actual_trace_length: warning.actual_trace_length, + maximum_trace_length: warning.maximum_trace_length, + })), + ).toEqual([ + { + pcb_trace_id: "pcb_trace_to_mcu", + actual_trace_length: 12, + maximum_trace_length: 10, + }, + { + pcb_trace_id: "pcb_trace_to_capacitor", + actual_trace_length: 11, + maximum_trace_length: 10, + }, + ]) +}) + +test("uses a configured crystal trace limit and preserves stricter trace limits", () => { + const constrainedCircuitJson = circuitJson.map((element) => { + if ( + element.type === "source_trace" && + element.source_trace_id === "crystal_trace" + ) { + return { ...element, max_length: 6 } + } + if ( + element.type === "source_trace" && + element.source_trace_id === "capacitor_trace" + ) { + return { ...element, max_length: 4 } + } + return element + }) as AnyCircuitElement[] + + const warnings = checkPcbTraceLengths(constrainedCircuitJson) + + expect( + warnings.map((warning) => [ + warning.pcb_trace_id, + warning.maximum_trace_length, + ]), + ).toEqual([ + ["pcb_trace_to_mcu", 6], + ["pcb_trace_to_capacitor", 4], + ]) +}) + +test("applies the default limit to a direct crystal trace without a connectivity key", () => { + const directCrystalCircuitJson = [ + circuitJson.find( + (element) => + element.type === "source_component" && + element.source_component_id === "crystal", + ), + circuitJson.find( + (element) => + element.type === "source_port" && + element.source_port_id === "crystal_port", + ), + { + type: "source_trace", + source_trace_id: "direct_crystal_trace", + connected_source_port_ids: ["crystal_port"], + connected_source_net_ids: [], + }, + { + type: "pcb_trace", + pcb_trace_id: "direct_crystal_pcb_trace", + source_trace_id: "direct_crystal_trace", + trace_length: 12, + route: [], + }, + ].filter(Boolean) as AnyCircuitElement[] + + expect(checkPcbTraceLengths(directCrystalCircuitJson)).toEqual([ + expect.objectContaining({ + pcb_trace_id: "direct_crystal_pcb_trace", + actual_trace_length: 12, + maximum_trace_length: 10, + }), + ]) +}) + +test("is included in the routing check pipeline", async () => { + const warnings = await runAllRoutingChecks(circuitJson) + + expect( + warnings.filter((warning) => warning.type === "pcb_trace_too_long_warning"), + ).toHaveLength(2) +}) From 82678912bd387feeef3097d9a20b321cad3bc8d6 Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 24 Jul 2026 10:41:52 -0700 Subject: [PATCH 2/2] Make PCB trace length checks generic --- lib/check-pcb-trace-lengths.ts | 79 +------- tests/lib/check-pcb-trace-lengths.test.ts | 215 ++++------------------ 2 files changed, 41 insertions(+), 253 deletions(-) diff --git a/lib/check-pcb-trace-lengths.ts b/lib/check-pcb-trace-lengths.ts index 71feb15..addc3df 100644 --- a/lib/check-pcb-trace-lengths.ts +++ b/lib/check-pcb-trace-lengths.ts @@ -3,17 +3,11 @@ import type { PcbTrace, PcbTraceRoutePoint, PcbTraceTooLongWarning, - SourcePort, SourceTrace, } from "circuit-json" -const DEFAULT_CRYSTAL_MAX_TRACE_LENGTH_MM = 10 const DEFAULT_VIA_LENGTH_MM = 1.6 -type SubcircuitConnectivityMapKey = NonNullable< - SourceTrace["subcircuit_connectivity_map_key"] -> - const getRoutePointPosition = (routePoint: PcbTraceRoutePoint) => routePoint.route_type === "through_pad" ? routePoint.start @@ -54,18 +48,6 @@ const getPcbTraceLength = (pcbTrace: PcbTrace): number => { export const checkPcbTraceLengths = ( circuitJson: AnyCircuitElement[], ): PcbTraceTooLongWarning[] => { - const crystalSourceComponentIds = new Set( - circuitJson - .filter( - (element) => - element.type === "source_component" && - element.ftype === "simple_crystal", - ) - .map((sourceComponent) => sourceComponent.source_component_id), - ) - const sourcePorts = circuitJson.filter( - (element): element is SourcePort => element.type === "source_port", - ) const sourceTraces = circuitJson.filter( (element): element is SourceTrace => element.type === "source_trace", ) @@ -73,56 +55,12 @@ export const checkPcbTraceLengths = ( (element): element is PcbTrace => element.type === "pcb_trace", ) - const sourcePortsById = new Map( - sourcePorts.map((sourcePort) => [sourcePort.source_port_id, sourcePort]), - ) const sourceTracesById = new Map( sourceTraces.map((sourceTrace) => [ sourceTrace.source_trace_id, sourceTrace, ]), ) - const crystalMaxLengthsBySubcircuitConnectivityMapKey = new Map< - SubcircuitConnectivityMapKey, - number - >() - const crystalMaxLengthsBySourceTraceId = new Map() - - for (const sourceTrace of sourceTraces) { - const isConnectedToCrystal = sourceTrace.connected_source_port_ids.some( - (sourcePortId) => { - const sourcePort = sourcePortsById.get(sourcePortId) - if (!sourcePort?.source_component_id) return false - - return crystalSourceComponentIds.has(sourcePort.source_component_id) - }, - ) - if (!isConnectedToCrystal) continue - - const crystalMaxLength = - sourceTrace.max_length ?? DEFAULT_CRYSTAL_MAX_TRACE_LENGTH_MM - crystalMaxLengthsBySourceTraceId.set( - sourceTrace.source_trace_id, - crystalMaxLength, - ) - - const subcircuitConnectivityMapKey = - sourceTrace.subcircuit_connectivity_map_key - if (!subcircuitConnectivityMapKey) continue - - const existingCrystalMaxLength = - crystalMaxLengthsBySubcircuitConnectivityMapKey.get( - subcircuitConnectivityMapKey, - ) - - crystalMaxLengthsBySubcircuitConnectivityMapKey.set( - subcircuitConnectivityMapKey, - existingCrystalMaxLength === undefined - ? crystalMaxLength - : Math.min(existingCrystalMaxLength, crystalMaxLength), - ) - } - const warnings: PcbTraceTooLongWarning[] = [] for (const pcbTrace of pcbTraces) { @@ -131,20 +69,9 @@ export const checkPcbTraceLengths = ( const sourceTrace = sourceTracesById.get(pcbTrace.source_trace_id) if (!sourceTrace) continue - const propagatedCrystalMaxLength = - sourceTrace.subcircuit_connectivity_map_key - ? crystalMaxLengthsBySubcircuitConnectivityMapKey.get( - sourceTrace.subcircuit_connectivity_map_key, - ) - : undefined - const maximumTraceLengths = [ - sourceTrace.max_length, - crystalMaxLengthsBySourceTraceId.get(sourceTrace.source_trace_id), - propagatedCrystalMaxLength, - ].filter((length): length is number => typeof length === "number") - if (maximumTraceLengths.length === 0) continue - - const maximumTraceLength = Math.min(...maximumTraceLengths) + const maximumTraceLength = sourceTrace.max_length + if (typeof maximumTraceLength !== "number") continue + const actualTraceLength = getPcbTraceLength(pcbTrace) if (actualTraceLength <= maximumTraceLength) continue diff --git a/tests/lib/check-pcb-trace-lengths.test.ts b/tests/lib/check-pcb-trace-lengths.test.ts index cc91675..c14ee65 100644 --- a/tests/lib/check-pcb-trace-lengths.test.ts +++ b/tests/lib/check-pcb-trace-lengths.test.ts @@ -3,97 +3,33 @@ import type { AnyCircuitElement } from "circuit-json" import { checkPcbTraceLengths, runAllRoutingChecks } from "../.." const circuitJson = [ - { - type: "source_component", - source_component_id: "crystal", - ftype: "simple_crystal", - name: "Y1", - frequency: 12_000_000, - }, - { - type: "source_component", - source_component_id: "mcu", - ftype: "simple_chip", - name: "U1", - }, - { - type: "source_component", - source_component_id: "load_capacitor", - ftype: "simple_capacitor", - name: "C1", - capacitance: 10e-12, - }, - { - type: "source_component", - source_component_id: "unrelated_resistor", - ftype: "simple_resistor", - name: "R1", - resistance: 1_000, - }, - { - type: "source_port", - source_port_id: "crystal_port", - source_component_id: "crystal", - name: "pin1", - pin_number: 1, - port_hints: ["1"], - }, - { - type: "source_port", - source_port_id: "mcu_port", - source_component_id: "mcu", - name: "XTAL_IN", - pin_number: 1, - port_hints: ["XTAL_IN"], - }, - { - type: "source_port", - source_port_id: "capacitor_port", - source_component_id: "load_capacitor", - name: "pin1", - pin_number: 1, - port_hints: ["1"], - }, - { - type: "source_port", - source_port_id: "resistor_port", - source_component_id: "unrelated_resistor", - name: "pin1", - pin_number: 1, - port_hints: ["1"], - }, { type: "source_trace", - source_trace_id: "crystal_trace", - connected_source_port_ids: ["crystal_port"], - connected_source_net_ids: ["crystal_net"], - subcircuit_connectivity_map_key: "board_crystal_net", + 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: "mcu_trace", - connected_source_port_ids: ["mcu_port"], - connected_source_net_ids: ["crystal_net"], - subcircuit_connectivity_map_key: "board_crystal_net", + source_trace_id: "short_source_trace", + connected_source_port_ids: [], + connected_source_net_ids: [], + max_length: 10, }, { type: "source_trace", - source_trace_id: "capacitor_trace", - connected_source_port_ids: ["capacitor_port"], - connected_source_net_ids: ["crystal_net"], - subcircuit_connectivity_map_key: "board_crystal_net", - }, - { - type: "source_trace", - source_trace_id: "unrelated_trace", - connected_source_port_ids: ["resistor_port"], - connected_source_net_ids: ["unrelated_net"], - subcircuit_connectivity_map_key: "board_unrelated_net", + source_trace_id: "unconstrained_source_trace", + connected_source_port_ids: [], + connected_source_net_ids: [], + max_length: null, }, { type: "pcb_trace", - pcb_trace_id: "pcb_trace_to_mcu", - source_trace_id: "mcu_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" }, @@ -101,117 +37,42 @@ const circuitJson = [ }, { type: "pcb_trace", - pcb_trace_id: "pcb_trace_to_capacitor", - source_trace_id: "capacitor_trace", - route: [ - { route_type: "wire", x: 0, y: 0, width: 0.15, layer: "top" }, - { route_type: "wire", x: 0, y: 11, width: 0.15, layer: "top" }, - ], + pcb_trace_id: "short_pcb_trace", + source_trace_id: "short_source_trace", + trace_length: 8, + route: [], }, { type: "pcb_trace", - pcb_trace_id: "pcb_trace_unrelated", - source_trace_id: "unrelated_trace", - route: [ - { route_type: "wire", x: 0, y: 0, width: 0.15, layer: "top" }, - { route_type: "wire", x: 20, y: 0, width: 0.15, layer: "top" }, - ], + pcb_trace_id: "unconstrained_pcb_trace", + source_trace_id: "unconstrained_source_trace", + trace_length: 20, + route: [], }, ] as AnyCircuitElement[] -test("warns for MCU and capacitor traces on a crystal net", () => { - const warnings = checkPcbTraceLengths(circuitJson) - - expect( - warnings.map((warning) => ({ - pcb_trace_id: warning.pcb_trace_id, - actual_trace_length: warning.actual_trace_length, - maximum_trace_length: warning.maximum_trace_length, - })), - ).toEqual([ +test("warns when a PCB trace exceeds its source trace maximum length", () => { + expect(checkPcbTraceLengths(circuitJson)).toEqual([ { - pcb_trace_id: "pcb_trace_to_mcu", + 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", }, - { - pcb_trace_id: "pcb_trace_to_capacitor", - actual_trace_length: 11, - maximum_trace_length: 10, - }, - ]) -}) - -test("uses a configured crystal trace limit and preserves stricter trace limits", () => { - const constrainedCircuitJson = circuitJson.map((element) => { - if ( - element.type === "source_trace" && - element.source_trace_id === "crystal_trace" - ) { - return { ...element, max_length: 6 } - } - if ( - element.type === "source_trace" && - element.source_trace_id === "capacitor_trace" - ) { - return { ...element, max_length: 4 } - } - return element - }) as AnyCircuitElement[] - - const warnings = checkPcbTraceLengths(constrainedCircuitJson) - - expect( - warnings.map((warning) => [ - warning.pcb_trace_id, - warning.maximum_trace_length, - ]), - ).toEqual([ - ["pcb_trace_to_mcu", 6], - ["pcb_trace_to_capacitor", 4], - ]) -}) - -test("applies the default limit to a direct crystal trace without a connectivity key", () => { - const directCrystalCircuitJson = [ - circuitJson.find( - (element) => - element.type === "source_component" && - element.source_component_id === "crystal", - ), - circuitJson.find( - (element) => - element.type === "source_port" && - element.source_port_id === "crystal_port", - ), - { - type: "source_trace", - source_trace_id: "direct_crystal_trace", - connected_source_port_ids: ["crystal_port"], - connected_source_net_ids: [], - }, - { - type: "pcb_trace", - pcb_trace_id: "direct_crystal_pcb_trace", - source_trace_id: "direct_crystal_trace", - trace_length: 12, - route: [], - }, - ].filter(Boolean) as AnyCircuitElement[] - - expect(checkPcbTraceLengths(directCrystalCircuitJson)).toEqual([ - expect.objectContaining({ - pcb_trace_id: "direct_crystal_pcb_trace", - actual_trace_length: 12, - maximum_trace_length: 10, - }), ]) }) test("is included in the routing check pipeline", async () => { - const warnings = await runAllRoutingChecks(circuitJson) + const results = await runAllRoutingChecks(circuitJson) expect( - warnings.filter((warning) => warning.type === "pcb_trace_too_long_warning"), - ).toHaveLength(2) + results.filter((result) => result.type === "pcb_trace_too_long_warning"), + ).toEqual(checkPcbTraceLengths(circuitJson)) })