Skip to content

Commit 6346ce3

Browse files
authored
Expand via DRC to inclusive declared layer spans (#96)
1 parent c7549d3 commit 6346ce3

9 files changed

Lines changed: 217 additions & 9 deletions

lib/drc/AutoroutingDrcEngine.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
SimplifiedPcbTrace,
1212
SimplifiedPcbTraces,
1313
} from "../types"
14+
import { getViaLayers } from "../utils/getViaLayers"
1415

1516
type Point = { x: number; y: number }
1617

@@ -615,7 +616,7 @@ export class AutoroutingDrcEngine {
615616
x: routePoint.x,
616617
y: routePoint.y,
617618
diameter: routePoint.via_diameter ?? this.srj.minViaDiameter ?? 0.3,
618-
layers: [routePoint.from_layer, routePoint.to_layer],
619+
layers: getViaLayers(routePoint, this.srj.layerCount),
619620
})
620621
}
621622
}

lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export type {
2424
SingleLayerConnectionPoint,
2525
SimpleRouteJson,
2626
} from "./solvers/GlobalDrcForceImproveSolver"
27+
export { getViaLayers } from "./utils/getViaLayers"

lib/utils/convertToCircuitJson.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { AnyCircuitElement, PcbTrace, PcbVia } from "circuit-json"
33
import type { Obstacle, SimpleRouteJson, SimplifiedPcbTrace } from "../types"
44
import type { HighDensityRoute } from "../types/high-density-types"
55
import { getConnectionPointLayers } from "../types/srj-types"
6+
import { getViaLayers } from "./getViaLayers"
67
import { mapZToLayerName } from "./mapZToLayerName"
78
import type { LayerName } from "./mapZToLayerName"
89
import { pointToBoxDistance } from "@tscircuit/math-utils"
@@ -57,6 +58,7 @@ function convertHdRouteToCircuitJsonTraces(
5758
baseId: string,
5859
connectionName: string,
5960
width = 0.1,
61+
layerCount: number,
6062
): PcbTrace[] {
6163
const traces: PcbTrace[] = []
6264

@@ -75,7 +77,7 @@ function convertHdRouteToCircuitJsonTraces(
7577
x: point.x,
7678
y: point.y,
7779
width,
78-
layer: mapZToLayerName(point.z, 2),
80+
layer: mapZToLayerName(point.z, layerCount),
7981
...(isFirstPoint && (point as any).pcb_port_id
8082
? { start_pcb_port_id: (point as any).pcb_port_id }
8183
: {}),
@@ -148,7 +150,7 @@ function convertHdRouteToCircuitJsonTraces(
148150
x: point.x,
149151
y: point.y,
150152
width,
151-
layer: mapZToLayerName(point.z, 2),
153+
layer: mapZToLayerName(point.z, layerCount),
152154
...(isFirstPoint && (point as any).pcb_port_id
153155
? { start_pcb_port_id: (point as any).pcb_port_id }
154156
: {}),
@@ -179,7 +181,7 @@ function convertHdRouteToCircuitJsonTraces(
179181
x: point.x,
180182
y: point.y,
181183
width,
182-
layer: mapZToLayerName(point.z, 2),
184+
layer: mapZToLayerName(point.z, layerCount),
183185
...(isLastPoint && (point as any).pcb_port_id
184186
? { end_pcb_port_id: (point as any).pcb_port_id }
185187
: {}),
@@ -454,6 +456,7 @@ function createPcbPadElements(srj: SimpleRouteJson): AnyCircuitElement[] {
454456
*/
455457
function extractViasFromRoutes(
456458
routes: SimplifiedPcbTrace[] | HighDensityRoute[],
459+
layerCount: number,
457460
minViaDiameter = 0.3,
458461
): PcbVia[] {
459462
const vias: PcbVia[] = []
@@ -476,7 +479,7 @@ function extractViasFromRoutes(
476479
y: segment.y,
477480
outer_diameter: viaDiameter,
478481
hole_diameter: viaDiameter * 0.5,
479-
layers: [segment.from_layer, segment.to_layer] as LayerName[],
482+
layers: getViaLayers(segment, layerCount) as LayerName[],
480483
})
481484
viaLocations.add(locationKey)
482485
}
@@ -498,8 +501,8 @@ function extractViasFromRoutes(
498501
Math.abs(prevPoint.x - currPoint.x) < 0.01 &&
499502
Math.abs(prevPoint.y - currPoint.y) < 0.01
500503
) {
501-
const fromLayer = mapZToLayerName(prevPoint.z, 2)
502-
const toLayer = mapZToLayerName(currPoint.z, 2)
504+
const fromLayer = mapZToLayerName(prevPoint.z, layerCount)
505+
const toLayer = mapZToLayerName(currPoint.z, layerCount)
503506
const locationKey = `${currPoint.x},${currPoint.y},${fromLayer},${toLayer}`
504507

505508
if (!viaLocations.has(locationKey)) {
@@ -511,7 +514,10 @@ function extractViasFromRoutes(
511514
y: currPoint.y,
512515
outer_diameter: viaDiameter,
513516
hole_diameter: viaDiameter * 0.5,
514-
layers: [fromLayer, toLayer] as LayerName[],
517+
layers: getViaLayers(
518+
{ from_layer: fromLayer, to_layer: toLayer },
519+
layerCount,
520+
) as LayerName[],
515521
})
516522
viaLocations.add(locationKey)
517523
}
@@ -550,7 +556,13 @@ export function convertToCircuitJson(
550556
circuitJson.push(...createPcbPadElements(srjWithPointPairs))
551557

552558
// Extract and add vias as independent pcb_via elements
553-
circuitJson.push(...extractViasFromRoutes(routes, minViaDiameter))
559+
circuitJson.push(
560+
...extractViasFromRoutes(
561+
routes,
562+
srjWithPointPairs.layerCount,
563+
minViaDiameter,
564+
),
565+
)
554566

555567
// Build a map of connection names to simplify lookups
556568
const connectionMap = new Map<string, string>()
@@ -583,6 +595,7 @@ export function convertToCircuitJson(
583595
`trace_${index}`,
584596
connectionMap.get(connectionName) || connectionName,
585597
minTraceWidth,
598+
srjWithPointPairs.layerCount,
586599
)
587600
circuitJson.push(...(traces as AnyCircuitElement[]))
588601
})

lib/utils/getViaLayers.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { mapZToLayerName } from "./mapZToLayerName"
2+
3+
type ViaLayers = {
4+
layers?: string[]
5+
from_layer?: string
6+
to_layer?: string
7+
}
8+
9+
/** Normalize the explicit-layer and inclusive-endpoint representations. */
10+
export const getViaLayers = (via: ViaLayers, layerCount: number): string[] => {
11+
if (via.layers !== undefined) return via.layers
12+
if (!Number.isInteger(layerCount) || layerCount < 1) {
13+
throw new Error(`Invalid board layer count: ${layerCount}`)
14+
}
15+
const boardLayers = Array.from({ length: layerCount }, (_, z) =>
16+
mapZToLayerName(z, layerCount),
17+
)
18+
const from = boardLayers.findIndex((layer) => layer === via.from_layer)
19+
const to = boardLayers.findIndex((layer) => layer === via.to_layer)
20+
if (from < 0 || to < 0) {
21+
throw new Error(
22+
`Via span ${via.from_layer} -> ${via.to_layer} is outside the board`,
23+
)
24+
}
25+
return boardLayers.slice(Math.min(from, to), Math.max(from, to) + 1)
26+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { expect, test } from "bun:test"
2+
import { AutoroutingDrcEngine } from "../lib/drc/AutoroutingDrcEngine"
3+
import type { SimpleRouteJson, SimplifiedPcbTraces } from "../lib/types"
4+
import { convertToCircuitJson } from "../lib/utils/convertToCircuitJson"
5+
6+
test("DRC treats endpoint spans and explicit via layers as the same geometry", () => {
7+
const srj: SimpleRouteJson = {
8+
layerCount: 4,
9+
minTraceWidth: 0.12,
10+
minViaDiameter: 0.2,
11+
bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 },
12+
obstacles: [],
13+
connections: [],
14+
}
15+
const traces: SimplifiedPcbTraces = [
16+
{
17+
type: "pcb_trace",
18+
pcb_trace_id: "power",
19+
connection_name: "power",
20+
route: [
21+
{
22+
route_type: "via",
23+
x: 0,
24+
y: 0,
25+
from_layer: "top",
26+
to_layer: "inner2",
27+
},
28+
],
29+
},
30+
{
31+
type: "pcb_trace",
32+
pcb_trace_id: "signal",
33+
connection_name: "signal",
34+
route: [
35+
{ route_type: "wire", x: -1, y: 0, width: 0.12, layer: "inner1" },
36+
{ route_type: "wire", x: 1, y: 0, width: 0.12, layer: "inner1" },
37+
],
38+
},
39+
]
40+
const engine = new AutoroutingDrcEngine(srj)
41+
expect(engine.evaluate(traces).errors.length).toBeGreaterThan(0)
42+
const declared = structuredClone(traces)
43+
const via = declared[0]!.route[0]!
44+
if (via.route_type !== "via") throw new Error("Expected a via")
45+
via.layers = ["top", "inner1", "inner2"]
46+
expect(engine.evaluate(declared).errors).toEqual(
47+
engine.evaluate(traces).errors,
48+
)
49+
via.layers = ["top", "inner1", "inner2", "bottom"]
50+
expect(engine.evaluate(declared).errors.length).toBeGreaterThan(0)
51+
for (const point of declared[1]!.route) {
52+
if (point.route_type === "wire") point.layer = "bottom"
53+
}
54+
expect(engine.evaluate(declared).errors.length).toBeGreaterThan(0)
55+
via.layers = ["top", "inner1", "inner2"]
56+
expect(engine.evaluate(declared).errors).toEqual([])
57+
const json = convertToCircuitJson(srj, declared)
58+
const exportedVia = json.find((element) => element.type === "pcb_via")
59+
expect(exportedVia?.layers).toEqual(["top", "inner1", "inner2"])
60+
expect(engine.evaluate(traces).errors.length).toBeGreaterThan(0)
61+
})
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { expect, test } from "bun:test"
2+
import { AutoroutingDrcEngine } from "../lib/drc/AutoroutingDrcEngine"
3+
import type { SimpleRouteJson, SimplifiedPcbTraces } from "../lib/types"
4+
5+
test("checks via-to-pad clearance on intermediate layers only within the span", () => {
6+
const srj: SimpleRouteJson = {
7+
layerCount: 4,
8+
minTraceWidth: 0.1,
9+
minViaDiameter: 0.3,
10+
bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 },
11+
connections: [],
12+
obstacles: [
13+
{
14+
type: "rect",
15+
layers: ["inner1"],
16+
center: { x: 0, y: 0 },
17+
width: 0.2,
18+
height: 0.2,
19+
connectedTo: ["pcb_smtpad_foreign"],
20+
},
21+
],
22+
}
23+
const traces: SimplifiedPcbTraces = [
24+
{
25+
type: "pcb_trace",
26+
pcb_trace_id: "power",
27+
connection_name: "power",
28+
route: [
29+
{
30+
route_type: "via",
31+
x: 0,
32+
y: 0,
33+
from_layer: "top",
34+
to_layer: "inner2",
35+
},
36+
],
37+
},
38+
]
39+
const input = structuredClone(traces)
40+
const errors = new AutoroutingDrcEngine(srj).evaluate(traces).errors
41+
expect(errors).toHaveLength(1)
42+
expect(errors[0]?.type).toBe("pcb_pad_pad_clearance_error")
43+
expect(traces).toEqual(input)
44+
45+
const via = traces[0]!.route[0]!
46+
if (via.route_type !== "via") throw new Error("Expected a via")
47+
via.layers = ["top", "inner1", "inner2"]
48+
expect(new AutoroutingDrcEngine(srj).evaluate(traces).errors).toEqual(errors)
49+
srj.obstacles[0]!.layers = ["bottom"]
50+
expect(new AutoroutingDrcEngine(srj).evaluate(traces).errors).toEqual([])
51+
})

tests/convert-hd-via-span.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect, test } from "bun:test"
2+
import type { SimpleRouteJson } from "../lib/types"
3+
import { convertToCircuitJson } from "../lib/utils/convertToCircuitJson"
4+
5+
test("HD conversion expands a four-layer via span using the actual stack", () => {
6+
const srj: SimpleRouteJson = {
7+
layerCount: 4,
8+
minTraceWidth: 0.1,
9+
bounds: { minX: -2, minY: -2, maxX: 2, maxY: 2 },
10+
obstacles: [],
11+
connections: [],
12+
}
13+
const json = convertToCircuitJson(srj, [
14+
{
15+
connectionName: "signal",
16+
traceThickness: 0.1,
17+
viaDiameter: 0.3,
18+
vias: [{ x: 0, y: 0 }],
19+
route: [
20+
{ x: -1, y: 0, z: 0 },
21+
{ x: 0, y: 0, z: 0 },
22+
{ x: 0, y: 0, z: 2 },
23+
{ x: 1, y: 0, z: 2 },
24+
],
25+
},
26+
])
27+
const via = json.find((element) => element.type === "pcb_via")
28+
expect(via?.layers).toEqual(["top", "inner1", "inner2"])
29+
const trace = json.find((element) => element.type === "pcb_trace")
30+
expect(trace?.route.at(-1)).toMatchObject({ layer: "inner2" })
31+
})

tests/via-layer-span.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expect, test } from "bun:test"
2+
import { getViaLayers } from "../lib/utils/getViaLayers"
3+
4+
test("normalizes both via APIs across the complete inclusive layer span", () => {
5+
const layers = ["top", "inner1", "inner2", "bottom"]
6+
expect(getViaLayers({ layers }, 4)).toBe(layers)
7+
expect(getViaLayers({ from_layer: "top", to_layer: "bottom" }, 4)).toEqual(
8+
layers,
9+
)
10+
expect(getViaLayers({ from_layer: "bottom", to_layer: "top" }, 4)).toEqual(
11+
layers,
12+
)
13+
expect(getViaLayers({ from_layer: "inner3", to_layer: "inner1" }, 6)).toEqual(
14+
["inner1", "inner2", "inner3"],
15+
)
16+
expect(getViaLayers({ from_layer: "inner1", to_layer: "inner1" }, 4)).toEqual(
17+
["inner1"],
18+
)
19+
expect(() =>
20+
getViaLayers({ from_layer: "inner4", to_layer: "top" }, 4),
21+
).toThrow("outside the board")
22+
})

types/srj-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ export interface SimplifiedPcbTrace {
111111
to_layer: string
112112
from_layer: string
113113
via_diameter?: number
114+
/** Explicit copper layers; otherwise expand the inclusive from/to span. */
115+
layers?: string[]
114116
}
115117
| {
116118
route_type: "jumper"

0 commit comments

Comments
 (0)