@tscircuit/fanout-solver 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -5
- package/lib/complete-original-endpoints.ts +32 -32
- package/lib/fanout-solver.ts +48 -57
- package/lib/index.ts +2 -0
- package/lib/prepare-buses.ts +26 -0
- package/lib/types.ts +23 -1
- package/lib/visualize-simple-route-json.ts +453 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,10 +83,11 @@ and treats each bus-layer decision atomically.
|
|
|
83
83
|
- `completeOriginalEndpoints` adds a bounded fail-first completion stage after
|
|
84
84
|
fanout. It first places DRC-gated interstitial capacitor escapes, then tries
|
|
85
85
|
breakout-to-pad routes with layer transitions at interior points along the
|
|
86
|
-
existing fanout copper, and finally
|
|
87
|
-
at original or moved routing
|
|
88
|
-
|
|
89
|
-
and the complete emitted
|
|
86
|
+
existing fanout copper, and finally calls the optional
|
|
87
|
+
`routeDownstreamConnections` host callback. Vias at original or moved routing
|
|
88
|
+
endpoints are rejected. A candidate is retained only when it improves
|
|
89
|
+
independently proven original endpoint connectivity and the complete emitted
|
|
90
|
+
copper remains DRC-clean.
|
|
90
91
|
- Emits supplied fanout traces, via obstacles, and moved breakout endpoints in a
|
|
91
92
|
new `SimpleRouteJson`. The returned problem is ready for a downstream
|
|
92
93
|
autorouter to finish.
|
|
@@ -102,7 +103,10 @@ bun add https://github.com/tscircuit/fanout-solver
|
|
|
102
103
|
## Usage
|
|
103
104
|
|
|
104
105
|
```ts
|
|
105
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
AutoroutingPipelineSolver6,
|
|
108
|
+
CapacityMeshSolver,
|
|
109
|
+
} from "@tscircuit/capacity-autorouter"
|
|
106
110
|
import { FanoutSolver } from "@tscircuit/fanout-solver"
|
|
107
111
|
|
|
108
112
|
const fanoutSolver = new FanoutSolver(simpleRouteJson, {
|
|
@@ -125,6 +129,17 @@ const fanoutSolver = new FanoutSolver(simpleRouteJson, {
|
|
|
125
129
|
availableCornersAndSides: ["top_left", "top", "top_right"],
|
|
126
130
|
borderDistribution: "even",
|
|
127
131
|
compactBusTracks: true,
|
|
132
|
+
completeOriginalEndpoints: true,
|
|
133
|
+
routeDownstreamConnections: (inputSrj, { effort }) => {
|
|
134
|
+
const downstreamSolver = new AutoroutingPipelineSolver6(inputSrj, {
|
|
135
|
+
effort,
|
|
136
|
+
})
|
|
137
|
+
downstreamSolver.solve()
|
|
138
|
+
if (!downstreamSolver.solved) {
|
|
139
|
+
throw new Error(downstreamSolver.error ?? "Downstream routing failed")
|
|
140
|
+
}
|
|
141
|
+
return downstreamSolver.getOutputSimpleRouteJson().traces ?? []
|
|
142
|
+
},
|
|
128
143
|
buses: [
|
|
129
144
|
{
|
|
130
145
|
busId: "ground",
|
|
@@ -146,6 +161,11 @@ const autorouter = new CapacityMeshSolver(
|
|
|
146
161
|
autorouter.solve()
|
|
147
162
|
```
|
|
148
163
|
|
|
164
|
+
The downstream callback is optional. It lets the application choose its
|
|
165
|
+
board-level router while keeping `@tscircuit/fanout-solver` free of a runtime
|
|
166
|
+
autorouter import. Returned traces are still accepted only after the fanout
|
|
167
|
+
solver's connectivity and copper-clearance checks pass.
|
|
168
|
+
|
|
149
169
|
The canonical bus input is the current `SimpleRouteJson` bus structure:
|
|
150
170
|
|
|
151
171
|
```ts
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type SimplifiedPcbTrace,
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionPoint,
|
|
3
|
+
Obstacle,
|
|
4
|
+
SimpleRouteJson,
|
|
5
|
+
SimplifiedPcbTrace,
|
|
7
6
|
} from "@tscircuit/capacity-autorouter"
|
|
8
7
|
import { createFanoutCompletionTraceId } from "./fanout-output-ids"
|
|
9
8
|
import {
|
|
@@ -15,6 +14,7 @@ import { getCopperLayerNames, getLayerSpan } from "./layer-names"
|
|
|
15
14
|
import { obstacleSharesElectricalNet } from "./net-identity"
|
|
16
15
|
import type {
|
|
17
16
|
FanoutEndpointCompletionReport,
|
|
17
|
+
FanoutDownstreamRouter,
|
|
18
18
|
FanoutRoutePlan,
|
|
19
19
|
Point2D,
|
|
20
20
|
} from "./types"
|
|
@@ -921,8 +921,9 @@ function findDownstreamTerminalBranch(params: {
|
|
|
921
921
|
|
|
922
922
|
/**
|
|
923
923
|
* Connects short opposite-layer terminal pairs with constrained interstitial
|
|
924
|
-
* vias, then delegates
|
|
925
|
-
* Only metric-improving, independently DRC-clean physical copper is
|
|
924
|
+
* vias, then delegates remaining long routes to an optional host-provided
|
|
925
|
+
* router. Only metric-improving, independently DRC-clean physical copper is
|
|
926
|
+
* retained.
|
|
926
927
|
*/
|
|
927
928
|
export function completeOriginalEndpoints(params: {
|
|
928
929
|
inputSrj: SimpleRouteJson
|
|
@@ -933,6 +934,7 @@ export function completeOriginalEndpoints(params: {
|
|
|
933
934
|
viaHoleDiameter: number
|
|
934
935
|
clearance: number
|
|
935
936
|
effort?: number
|
|
937
|
+
routeDownstreamConnections?: FanoutDownstreamRouter
|
|
936
938
|
}): CompleteOriginalEndpointsResult {
|
|
937
939
|
const {
|
|
938
940
|
inputSrj,
|
|
@@ -943,6 +945,7 @@ export function completeOriginalEndpoints(params: {
|
|
|
943
945
|
viaHoleDiameter,
|
|
944
946
|
clearance,
|
|
945
947
|
effort = 1,
|
|
948
|
+
routeDownstreamConnections,
|
|
946
949
|
} = params
|
|
947
950
|
const errors: string[] = []
|
|
948
951
|
const baselineDrc = validateRoutedCopperDrc({
|
|
@@ -1072,7 +1075,8 @@ export function completeOriginalEndpoints(params: {
|
|
|
1072
1075
|
if (
|
|
1073
1076
|
baselineDrc.valid &&
|
|
1074
1077
|
downstreamConnections.length > 0 &&
|
|
1075
|
-
downstreamConnections.length <= 12
|
|
1078
|
+
downstreamConnections.length <= 12 &&
|
|
1079
|
+
routeDownstreamConnections
|
|
1076
1080
|
) {
|
|
1077
1081
|
const downstreamConnectionNames = new Set(
|
|
1078
1082
|
downstreamConnections.map((connection) => connection.name),
|
|
@@ -1095,36 +1099,32 @@ export function completeOriginalEndpoints(params: {
|
|
|
1095
1099
|
traces: [],
|
|
1096
1100
|
}
|
|
1097
1101
|
try {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1102
|
+
const candidates = routeDownstreamConnections(downstreamInput, { effort })
|
|
1103
|
+
downstreamTraces = acceptDownstreamTraces({
|
|
1104
|
+
inputSrj,
|
|
1105
|
+
fanoutSrj,
|
|
1106
|
+
localTraces: [...bestLocalAttempt.traces, ...directDownstreamTraces],
|
|
1107
|
+
candidates: candidates.filter((trace) =>
|
|
1108
|
+
downstreamConnectionNames.has(trace.connection_name),
|
|
1109
|
+
),
|
|
1110
|
+
clearance,
|
|
1100
1111
|
})
|
|
1101
|
-
downstreamSolver.solve()
|
|
1102
|
-
if (downstreamSolver.solved) {
|
|
1103
|
-
downstreamTraces = acceptDownstreamTraces({
|
|
1104
|
-
inputSrj,
|
|
1105
|
-
fanoutSrj,
|
|
1106
|
-
localTraces: [...bestLocalAttempt.traces, ...directDownstreamTraces],
|
|
1107
|
-
candidates:
|
|
1108
|
-
downstreamSolver
|
|
1109
|
-
.getOutputSimpleRouteJson()
|
|
1110
|
-
.traces?.filter((trace) =>
|
|
1111
|
-
downstreamConnectionNames.has(trace.connection_name),
|
|
1112
|
-
) ?? [],
|
|
1113
|
-
clearance,
|
|
1114
|
-
})
|
|
1115
|
-
} else {
|
|
1116
|
-
errors.push(
|
|
1117
|
-
`Downstream autorouter did not solve: ${downstreamSolver.error ?? "unknown error"}`,
|
|
1118
|
-
)
|
|
1119
|
-
}
|
|
1120
1112
|
} catch (error) {
|
|
1121
1113
|
errors.push(
|
|
1122
|
-
`Downstream
|
|
1114
|
+
`Downstream router failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1123
1115
|
)
|
|
1124
1116
|
}
|
|
1117
|
+
} else if (!baselineDrc.valid && downstreamConnections.length > 0) {
|
|
1118
|
+
errors.push(
|
|
1119
|
+
"Skipped downstream router because the endpoint-completion baseline failed emitted-copper DRC",
|
|
1120
|
+
)
|
|
1125
1121
|
} else if (downstreamConnections.length > 12) {
|
|
1126
1122
|
errors.push(
|
|
1127
|
-
`Skipped downstream
|
|
1123
|
+
`Skipped downstream router for ${downstreamConnections.length} unresolved connections (bounded at 12)`,
|
|
1124
|
+
)
|
|
1125
|
+
} else if (downstreamConnections.length > 0 && !routeDownstreamConnections) {
|
|
1126
|
+
errors.push(
|
|
1127
|
+
`Skipped downstream router for ${downstreamConnections.length} unresolved connections because no routeDownstreamConnections callback was provided`,
|
|
1128
1128
|
)
|
|
1129
1129
|
}
|
|
1130
1130
|
|
package/lib/fanout-solver.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
convertSrjToGraphicsObject,
|
|
3
|
-
type SimpleRouteJson,
|
|
4
|
-
} from "@tscircuit/capacity-autorouter"
|
|
1
|
+
import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
|
|
5
2
|
import { BaseSolver } from "@tscircuit/solver-utils"
|
|
6
3
|
import type { GraphicsObject } from "graphics-debug"
|
|
7
4
|
import { buildOutputSimpleRouteJson } from "./build-output"
|
|
@@ -9,7 +6,6 @@ import {
|
|
|
9
6
|
completeOriginalEndpoints,
|
|
10
7
|
type CompleteOriginalEndpointsResult,
|
|
11
8
|
} from "./complete-original-endpoints"
|
|
12
|
-
import { getCopperLayerColor } from "./layer-colors"
|
|
13
9
|
import { generateLayerAssignments, getCopperLayerNames } from "./layer-names"
|
|
14
10
|
import {
|
|
15
11
|
prepareFanoutBuses,
|
|
@@ -23,6 +19,7 @@ import {
|
|
|
23
19
|
import { routeSingleLayerWithAdaptiveExits } from "./route-single-layer-adaptive-exits"
|
|
24
20
|
import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
|
|
25
21
|
import { validateFanoutSolution } from "./validate-fanout-solution"
|
|
22
|
+
import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
|
|
26
23
|
import type {
|
|
27
24
|
AssignmentAttempt,
|
|
28
25
|
Bounds,
|
|
@@ -262,7 +259,7 @@ function getBusDepthInRows(bus: PreparedBus): number {
|
|
|
262
259
|
)
|
|
263
260
|
}
|
|
264
261
|
|
|
265
|
-
function
|
|
262
|
+
function createInitialLayerAssignment(params: {
|
|
266
263
|
buses: PreparedBus[]
|
|
267
264
|
escapeLayers: string[]
|
|
268
265
|
escapeLayersByBusId: Readonly<Record<string, readonly string[]>>
|
|
@@ -317,16 +314,16 @@ function createPreferredLayerAssignment(params: {
|
|
|
317
314
|
}
|
|
318
315
|
|
|
319
316
|
function prioritizeLayerAssignment(params: {
|
|
320
|
-
|
|
317
|
+
initialAssignment: Readonly<Record<string, string>>
|
|
321
318
|
generatedAssignments: Array<Readonly<Record<string, string>>>
|
|
322
319
|
maxAssignments: number
|
|
323
320
|
}): Array<Readonly<Record<string, string>>> {
|
|
324
|
-
const {
|
|
325
|
-
const
|
|
321
|
+
const { initialAssignment, generatedAssignments, maxAssignments } = params
|
|
322
|
+
const initialKey = JSON.stringify(initialAssignment)
|
|
326
323
|
return [
|
|
327
|
-
|
|
324
|
+
initialAssignment,
|
|
328
325
|
...generatedAssignments.filter(
|
|
329
|
-
(assignment) => JSON.stringify(assignment) !==
|
|
326
|
+
(assignment) => JSON.stringify(assignment) !== initialKey,
|
|
330
327
|
),
|
|
331
328
|
].slice(0, maxAssignments)
|
|
332
329
|
}
|
|
@@ -338,7 +335,12 @@ function getCandidateEscapeLayersForBus(params: {
|
|
|
338
335
|
staticClearanceCache: RouteBusStaticClearanceCache
|
|
339
336
|
}): string[] {
|
|
340
337
|
const { bus, srj, config, staticClearanceCache } = params
|
|
341
|
-
const
|
|
338
|
+
const busAllowedLayers = bus.allowedLayers
|
|
339
|
+
const allowedEscapeLayers =
|
|
340
|
+
busAllowedLayers === undefined
|
|
341
|
+
? config.escapeLayers
|
|
342
|
+
: config.escapeLayers.filter((layer) => busAllowedLayers.includes(layer))
|
|
343
|
+
const individuallyRoutableLayers = allowedEscapeLayers.filter(
|
|
342
344
|
(targetLayer) =>
|
|
343
345
|
routeBus({
|
|
344
346
|
srj,
|
|
@@ -360,9 +362,11 @@ function getCandidateEscapeLayersForBus(params: {
|
|
|
360
362
|
// route this bus by itself cannot become viable later in an assignment.
|
|
361
363
|
// Preserve the original candidates when none route so impossible problems
|
|
362
364
|
// still produce the usual failed-solver result instead of throwing here.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
365
|
+
const candidateLayers =
|
|
366
|
+
individuallyRoutableLayers.length > 0
|
|
367
|
+
? individuallyRoutableLayers
|
|
368
|
+
: allowedEscapeLayers
|
|
369
|
+
return candidateLayers
|
|
366
370
|
}
|
|
367
371
|
|
|
368
372
|
export class FanoutSolver extends BaseSolver {
|
|
@@ -403,6 +407,24 @@ export class FanoutSolver extends BaseSolver {
|
|
|
403
407
|
this.config = resolveConfig(inputSrj, options)
|
|
404
408
|
this.preparedBuses = prepareFanoutBuses(inputSrj, options)
|
|
405
409
|
for (const bus of this.preparedBuses) {
|
|
410
|
+
for (const allowedLayer of bus.allowedLayers ?? []) {
|
|
411
|
+
if (!this.config.layerNames.includes(allowedLayer)) {
|
|
412
|
+
throw new Error(
|
|
413
|
+
`FanoutSolver: bus "${bus.busId}" allows unavailable layer "${allowedLayer}"`,
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (
|
|
418
|
+
bus.termination.type === "boundary" &&
|
|
419
|
+
bus.allowedLayers !== undefined &&
|
|
420
|
+
!bus.allowedLayers.some((layer) =>
|
|
421
|
+
this.config.escapeLayers.includes(layer),
|
|
422
|
+
)
|
|
423
|
+
) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`FanoutSolver: bus "${bus.busId}" has no allowed layer in escapeLayers`,
|
|
426
|
+
)
|
|
427
|
+
}
|
|
406
428
|
if (bus.termination.type !== "plane") continue
|
|
407
429
|
const planeLayer = bus.termination.layer
|
|
408
430
|
if (!this.config.layerNames.includes(planeLayer)) {
|
|
@@ -410,6 +432,14 @@ export class FanoutSolver extends BaseSolver {
|
|
|
410
432
|
`FanoutSolver: plane-terminated bus "${bus.busId}" targets unavailable layer "${planeLayer}"`,
|
|
411
433
|
)
|
|
412
434
|
}
|
|
435
|
+
if (
|
|
436
|
+
bus.allowedLayers !== undefined &&
|
|
437
|
+
!bus.allowedLayers.includes(planeLayer)
|
|
438
|
+
) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`FanoutSolver: plane-terminated bus "${bus.busId}" targets disallowed layer "${planeLayer}"`,
|
|
441
|
+
)
|
|
442
|
+
}
|
|
413
443
|
if (
|
|
414
444
|
bus.connections.some(
|
|
415
445
|
(connection) => connection.sourceLayer === planeLayer,
|
|
@@ -457,7 +487,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
457
487
|
...fixedPlaneAssignments,
|
|
458
488
|
}))
|
|
459
489
|
this.layerAssignments = prioritizeLayerAssignment({
|
|
460
|
-
|
|
490
|
+
initialAssignment: createInitialLayerAssignment({
|
|
461
491
|
buses: this.preparedBuses,
|
|
462
492
|
escapeLayers: this.config.escapeLayers,
|
|
463
493
|
escapeLayersByBusId,
|
|
@@ -489,6 +519,7 @@ export class FanoutSolver extends BaseSolver {
|
|
|
489
519
|
viaHoleDiameter: this.config.viaHoleDiameter,
|
|
490
520
|
clearance: this.config.clearance,
|
|
491
521
|
effort: this.options.endpointCompletionEffort,
|
|
522
|
+
routeDownstreamConnections: this.options.routeDownstreamConnections,
|
|
492
523
|
})
|
|
493
524
|
}
|
|
494
525
|
|
|
@@ -1254,46 +1285,6 @@ export class FanoutSolver extends BaseSolver {
|
|
|
1254
1285
|
this.endpointCompletion?.simpleRouteJson ??
|
|
1255
1286
|
this.bestAttempt?.outputSrj ??
|
|
1256
1287
|
this.inputSrj
|
|
1257
|
-
|
|
1258
|
-
const circularPadKeys = new Set(
|
|
1259
|
-
visualizedSrj.obstacles
|
|
1260
|
-
.filter(
|
|
1261
|
-
(obstacle) =>
|
|
1262
|
-
(obstacle as typeof obstacle & { shape?: string }).shape ===
|
|
1263
|
-
"circle",
|
|
1264
|
-
)
|
|
1265
|
-
.map(
|
|
1266
|
-
(obstacle) =>
|
|
1267
|
-
`${obstacle.center.x}:${obstacle.center.y}:${obstacle.width}:${obstacle.height}`,
|
|
1268
|
-
),
|
|
1269
|
-
)
|
|
1270
|
-
const circularPadGraphics: NonNullable<GraphicsObject["circles"]> = []
|
|
1271
|
-
const rects = graphics.rects?.filter((rect) => {
|
|
1272
|
-
const key = `${rect.center.x}:${rect.center.y}:${rect.width}:${rect.height}`
|
|
1273
|
-
if (!circularPadKeys.has(key)) return true
|
|
1274
|
-
circularPadGraphics.push({
|
|
1275
|
-
center: rect.center,
|
|
1276
|
-
radius: Math.min(rect.width, rect.height) / 2,
|
|
1277
|
-
fill: rect.fill,
|
|
1278
|
-
stroke: rect.stroke,
|
|
1279
|
-
layer: rect.layer,
|
|
1280
|
-
label: rect.label,
|
|
1281
|
-
})
|
|
1282
|
-
return false
|
|
1283
|
-
})
|
|
1284
|
-
return {
|
|
1285
|
-
...graphics,
|
|
1286
|
-
rects,
|
|
1287
|
-
circles: [...(graphics.circles ?? []), ...circularPadGraphics],
|
|
1288
|
-
lines: graphics.lines?.map((line) => {
|
|
1289
|
-
const layerMatch = /^z(\d+)$/.exec(line.layer ?? "")
|
|
1290
|
-
if (!layerMatch) return line
|
|
1291
|
-
const { strokeDash: _strokeDash, ...solidLine } = line
|
|
1292
|
-
return {
|
|
1293
|
-
...solidLine,
|
|
1294
|
-
strokeColor: getCopperLayerColor(Number(layerMatch[1])),
|
|
1295
|
-
}
|
|
1296
|
-
}),
|
|
1297
|
-
}
|
|
1288
|
+
return visualizeSimpleRouteJson(visualizedSrj)
|
|
1298
1289
|
}
|
|
1299
1290
|
}
|
package/lib/index.ts
CHANGED
package/lib/prepare-buses.ts
CHANGED
|
@@ -407,6 +407,26 @@ function resolvePreferredExit(
|
|
|
407
407
|
return value
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
function resolveAllowedLayers(
|
|
411
|
+
busId: string,
|
|
412
|
+
allowedLayers: readonly string[] | undefined,
|
|
413
|
+
): string[] | undefined {
|
|
414
|
+
if (allowedLayers === undefined) return undefined
|
|
415
|
+
if (allowedLayers.length === 0) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`FanoutSolver: bus "${busId}" must allow at least one layer`,
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
for (const layer of allowedLayers) {
|
|
421
|
+
if (typeof layer !== "string" || layer.length === 0) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`FanoutSolver: bus "${busId}" has an invalid allowed layer`,
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return [...new Set(allowedLayers)]
|
|
428
|
+
}
|
|
429
|
+
|
|
410
430
|
export function resolveAvailableBoundaryRegions(
|
|
411
431
|
value: readonly FanoutAvailableCornerAndSideInput[] | undefined,
|
|
412
432
|
): AvailableBoundaryRegion[] | undefined {
|
|
@@ -491,6 +511,10 @@ function resolveBusSpecs(
|
|
|
491
511
|
(requestedBus as FanoutBusSpec).preferredExit ??
|
|
492
512
|
options.defaultPreferredExit,
|
|
493
513
|
)
|
|
514
|
+
const allowedLayers = resolveAllowedLayers(
|
|
515
|
+
requestedBus.busId,
|
|
516
|
+
(requestedBus as FanoutBusSpec).allowedLayers,
|
|
517
|
+
)
|
|
494
518
|
if (termination.type === "plane" && preferredExit !== undefined) {
|
|
495
519
|
throw new Error(
|
|
496
520
|
`FanoutSolver: plane-terminated bus "${requestedBus.busId}" cannot also specify preferredExit`,
|
|
@@ -506,6 +530,7 @@ function resolveBusSpecs(
|
|
|
506
530
|
(requestedBus as FanoutBusSpec).direction ??
|
|
507
531
|
options.defaultDirection,
|
|
508
532
|
preferredExit,
|
|
533
|
+
...(allowedLayers === undefined ? {} : { allowedLayers }),
|
|
509
534
|
termination,
|
|
510
535
|
})
|
|
511
536
|
}
|
|
@@ -1020,6 +1045,7 @@ export function prepareFanoutBuses(
|
|
|
1020
1045
|
busId: busSpec.busId,
|
|
1021
1046
|
direction: resolvedExit.direction,
|
|
1022
1047
|
preferredExit: resolvedExit.preferredExit,
|
|
1048
|
+
allowedLayers: busSpec.allowedLayers,
|
|
1023
1049
|
termination: busSpec.termination ?? { type: "boundary" },
|
|
1024
1050
|
connections: preparedConnections,
|
|
1025
1051
|
componentId: sourceGrid.componentId,
|
package/lib/types.ts
CHANGED
|
@@ -49,6 +49,19 @@ export type FanoutAvailableCornerAndSideInput =
|
|
|
49
49
|
|
|
50
50
|
export type FanoutBorderDistribution = "preserve" | "even"
|
|
51
51
|
|
|
52
|
+
export interface FanoutDownstreamRouterOptions {
|
|
53
|
+
effort: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Optional host-provided router for unresolved connections after the fanout
|
|
58
|
+
* solver's local endpoint-completion passes.
|
|
59
|
+
*/
|
|
60
|
+
export type FanoutDownstreamRouter = (
|
|
61
|
+
inputSrj: SimpleRouteJson,
|
|
62
|
+
options: FanoutDownstreamRouterOptions,
|
|
63
|
+
) => SimplifiedPcbTrace[]
|
|
64
|
+
|
|
52
65
|
export type FanoutBusTermination =
|
|
53
66
|
| {
|
|
54
67
|
type: "boundary"
|
|
@@ -63,6 +76,8 @@ export interface FanoutBusSpec extends SimpleRouteBus {
|
|
|
63
76
|
sourceComponentId?: string
|
|
64
77
|
direction?: FanoutDirection
|
|
65
78
|
preferredExit?: FanoutBorderTarget
|
|
79
|
+
/** Layers to which this bus is allowed to escape. */
|
|
80
|
+
allowedLayers?: readonly string[]
|
|
66
81
|
/**
|
|
67
82
|
* Preferred downstream routing point for each connection after it leaves the
|
|
68
83
|
* fanout boundary. This is routing guidance only and does not replace the
|
|
@@ -127,8 +142,13 @@ export interface FanoutSolverOptions {
|
|
|
127
142
|
* with the independent endpoint-connectivity and emitted-copper validators.
|
|
128
143
|
*/
|
|
129
144
|
completeOriginalEndpoints?: boolean
|
|
130
|
-
/** Effort passed to the bounded downstream
|
|
145
|
+
/** Effort passed to the optional bounded downstream-router pass. */
|
|
131
146
|
endpointCompletionEffort?: number
|
|
147
|
+
/**
|
|
148
|
+
* Host-provided fallback for unresolved endpoint connections. The fanout
|
|
149
|
+
* package deliberately does not import a board-level autorouter at runtime.
|
|
150
|
+
*/
|
|
151
|
+
routeDownstreamConnections?: FanoutDownstreamRouter
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
export interface FanoutAttemptSummary {
|
|
@@ -226,6 +246,8 @@ export interface PreparedBus {
|
|
|
226
246
|
busId: string
|
|
227
247
|
direction: FanoutDirection
|
|
228
248
|
preferredExit?: FanoutBorderTarget
|
|
249
|
+
/** Layers to which this bus is allowed to escape. */
|
|
250
|
+
allowedLayers?: readonly string[]
|
|
229
251
|
termination: FanoutBusTermination
|
|
230
252
|
connections: PreparedConnection[]
|
|
231
253
|
componentId: string
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionPoint,
|
|
3
|
+
Obstacle,
|
|
4
|
+
SimpleRouteJson,
|
|
5
|
+
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import type { Circle, GraphicsObject, Line, Point, Rect } from "graphics-debug"
|
|
7
|
+
import { getCopperLayerColor } from "./layer-colors"
|
|
8
|
+
import { getCopperLayerNames } from "./layer-names"
|
|
9
|
+
|
|
10
|
+
const LEGACY_VISUALIZATION_LAYERS = new Set([
|
|
11
|
+
"top",
|
|
12
|
+
"bottom",
|
|
13
|
+
"inner1",
|
|
14
|
+
"inner2",
|
|
15
|
+
"inner3",
|
|
16
|
+
"inner4",
|
|
17
|
+
"inner5",
|
|
18
|
+
"inner6",
|
|
19
|
+
"inner7",
|
|
20
|
+
"inner8",
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const JUMPER_DIMENSIONS = {
|
|
24
|
+
"0603": { padLength: 0.8, padWidth: 0.95 },
|
|
25
|
+
"1206": { padLength: 0.6, padWidth: 1.6 },
|
|
26
|
+
"1206x4_pair": { padLength: 0.8, padWidth: 0.5 },
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
const getLayerIndex = (layerNames: string[], layerName: string): number => {
|
|
30
|
+
const layerIndex = layerNames.indexOf(layerName)
|
|
31
|
+
if (layerIndex < 0) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`FanoutSolver: cannot visualize unknown copper layer "${layerName}"`,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
return layerIndex
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const getGraphicsLayer = (
|
|
40
|
+
layerNames: string[],
|
|
41
|
+
copperLayers: readonly string[],
|
|
42
|
+
): string => {
|
|
43
|
+
const zLayers = [
|
|
44
|
+
...new Set(
|
|
45
|
+
copperLayers.map((layerName) => getLayerIndex(layerNames, layerName)),
|
|
46
|
+
),
|
|
47
|
+
].sort((first, second) => first - second)
|
|
48
|
+
return `z${zLayers.join(",")}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const getPointLayers = (point: ConnectionPoint): string[] => {
|
|
52
|
+
const layers = "layers" in point ? point.layers : undefined
|
|
53
|
+
if (layers && layers.length > 0) return layers
|
|
54
|
+
return [(point as ConnectionPoint & { layer: string }).layer]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const getObstacleLayerIndexes = (
|
|
58
|
+
obstacle: Obstacle,
|
|
59
|
+
layerNames: string[],
|
|
60
|
+
): number[] => {
|
|
61
|
+
if (obstacle.__zLayers && obstacle.__zLayers.length > 0) {
|
|
62
|
+
return [...new Set(obstacle.__zLayers)]
|
|
63
|
+
.filter(
|
|
64
|
+
(layerIndex) =>
|
|
65
|
+
Number.isInteger(layerIndex) &&
|
|
66
|
+
layerIndex >= 0 &&
|
|
67
|
+
layerIndex < layerNames.length,
|
|
68
|
+
)
|
|
69
|
+
.sort((first, second) => first - second)
|
|
70
|
+
}
|
|
71
|
+
return [
|
|
72
|
+
...new Set(
|
|
73
|
+
obstacle.layers.map((layerName) => getLayerIndex(layerNames, layerName)),
|
|
74
|
+
),
|
|
75
|
+
].sort((first, second) => first - second)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getViaLayerNames = (
|
|
79
|
+
layerNames: string[],
|
|
80
|
+
fromLayer: string,
|
|
81
|
+
toLayer: string,
|
|
82
|
+
): string[] => {
|
|
83
|
+
const fromIndex = getLayerIndex(layerNames, fromLayer)
|
|
84
|
+
const toIndex = getLayerIndex(layerNames, toLayer)
|
|
85
|
+
return layerNames.slice(
|
|
86
|
+
Math.min(fromIndex, toIndex),
|
|
87
|
+
Math.max(fromIndex, toIndex) + 1,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const firstFiniteNumber = (
|
|
92
|
+
...values: Array<number | undefined>
|
|
93
|
+
): number | undefined =>
|
|
94
|
+
values.find((value) => typeof value === "number" && Number.isFinite(value))
|
|
95
|
+
|
|
96
|
+
const getViaPadDiameter = (srj: SimpleRouteJson): number => {
|
|
97
|
+
const holeDiameter = firstFiniteNumber(
|
|
98
|
+
srj.min_via_hole_diameter,
|
|
99
|
+
srj.minViaHoleDiameter,
|
|
100
|
+
)
|
|
101
|
+
const padDiameter = firstFiniteNumber(
|
|
102
|
+
srj.min_via_pad_diameter,
|
|
103
|
+
srj.minViaPadDiameter,
|
|
104
|
+
srj.minViaDiameter,
|
|
105
|
+
)
|
|
106
|
+
return Math.max(padDiameter ?? srj.minViaDiameter ?? 0.3, holeDiameter ?? 0)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const getColorMap = (
|
|
110
|
+
connections: SimpleRouteJson["connections"],
|
|
111
|
+
): Record<string, string> =>
|
|
112
|
+
Object.fromEntries(
|
|
113
|
+
connections.map((connection, index) => [
|
|
114
|
+
connection.name,
|
|
115
|
+
`hsl(${(index * 340) / connections.length}, 100%, 50%)`,
|
|
116
|
+
]),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const hslToRgb = (hue: number, saturation: number, lightness: number) => {
|
|
120
|
+
const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation
|
|
121
|
+
const hueSegment = (((hue % 360) + 360) % 360) / 60
|
|
122
|
+
const secondary = chroma * (1 - Math.abs((hueSegment % 2) - 1))
|
|
123
|
+
const [red, green, blue] =
|
|
124
|
+
hueSegment < 1
|
|
125
|
+
? [chroma, secondary, 0]
|
|
126
|
+
: hueSegment < 2
|
|
127
|
+
? [secondary, chroma, 0]
|
|
128
|
+
: hueSegment < 3
|
|
129
|
+
? [0, chroma, secondary]
|
|
130
|
+
: hueSegment < 4
|
|
131
|
+
? [0, secondary, chroma]
|
|
132
|
+
: hueSegment < 5
|
|
133
|
+
? [secondary, 0, chroma]
|
|
134
|
+
: [chroma, 0, secondary]
|
|
135
|
+
const match = lightness - chroma / 2
|
|
136
|
+
return [red, green, blue].map((channel) =>
|
|
137
|
+
Math.round((channel + match) * 255),
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const transparentize = (color: string, amount: number): string => {
|
|
142
|
+
const namedColors: Record<string, [number, number, number]> = {
|
|
143
|
+
blue: [0, 0, 255],
|
|
144
|
+
orange: [255, 165, 0],
|
|
145
|
+
purple: [128, 0, 128],
|
|
146
|
+
red: [255, 0, 0],
|
|
147
|
+
}
|
|
148
|
+
let channels = namedColors[color]
|
|
149
|
+
let alpha = 1
|
|
150
|
+
const rgbaMatch =
|
|
151
|
+
/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/.exec(
|
|
152
|
+
color,
|
|
153
|
+
)
|
|
154
|
+
if (rgbaMatch) {
|
|
155
|
+
channels = [
|
|
156
|
+
Number(rgbaMatch[1]),
|
|
157
|
+
Number(rgbaMatch[2]),
|
|
158
|
+
Number(rgbaMatch[3]),
|
|
159
|
+
]
|
|
160
|
+
alpha = rgbaMatch[4] === undefined ? 1 : Number(rgbaMatch[4])
|
|
161
|
+
}
|
|
162
|
+
const hslMatch =
|
|
163
|
+
/^hsl\(\s*([\d.-]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*\)$/.exec(color)
|
|
164
|
+
if (hslMatch) {
|
|
165
|
+
channels = hslToRgb(
|
|
166
|
+
Number(hslMatch[1]),
|
|
167
|
+
Number(hslMatch[2]) / 100,
|
|
168
|
+
Number(hslMatch[3]) / 100,
|
|
169
|
+
) as [number, number, number]
|
|
170
|
+
}
|
|
171
|
+
if (!channels) return color
|
|
172
|
+
const outputAlpha = +Math.max(0, alpha * 100 - amount * 100).toFixed(2) / 100
|
|
173
|
+
if (outputAlpha >= 1) {
|
|
174
|
+
const hex = channels
|
|
175
|
+
.map((channel) => Math.round(channel).toString(16).padStart(2, "0"))
|
|
176
|
+
.join("")
|
|
177
|
+
return hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]
|
|
178
|
+
? `#${hex[0]}${hex[2]}${hex[4]}`
|
|
179
|
+
: `#${hex}`
|
|
180
|
+
}
|
|
181
|
+
return `rgba(${channels.join(",")},${outputAlpha})`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const getUniqueValues = (values: readonly string[]): string[] => {
|
|
185
|
+
const seen = new Set<string>()
|
|
186
|
+
return values.filter((value) => {
|
|
187
|
+
if (seen.has(value)) return false
|
|
188
|
+
seen.add(value)
|
|
189
|
+
return true
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const createObstacleLabelFormatter = (srj: SimpleRouteJson) => {
|
|
194
|
+
const rootConnectionIndex = new Map<string, string[]>()
|
|
195
|
+
const addMapping = (identifier: string | undefined, rootName: string) => {
|
|
196
|
+
if (!identifier) return
|
|
197
|
+
const names = rootConnectionIndex.get(identifier) ?? []
|
|
198
|
+
if (!names.includes(rootName)) names.push(rootName)
|
|
199
|
+
rootConnectionIndex.set(identifier, names)
|
|
200
|
+
}
|
|
201
|
+
for (const connection of srj.connections) {
|
|
202
|
+
const rootNames = connection.__rootConnectionNames ?? [connection.name]
|
|
203
|
+
for (const rootName of rootNames) {
|
|
204
|
+
addMapping(connection.name, rootName)
|
|
205
|
+
addMapping(rootName, rootName)
|
|
206
|
+
addMapping(connection.__netConnectionName, rootName)
|
|
207
|
+
for (const point of connection.pointsToConnect) {
|
|
208
|
+
addMapping(point.pointId, rootName)
|
|
209
|
+
addMapping(point.pcb_port_id, rootName)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return (obstacle: Obstacle): string => {
|
|
214
|
+
const rootNames = getUniqueValues([
|
|
215
|
+
...obstacle.connectedTo.flatMap(
|
|
216
|
+
(identifier) => rootConnectionIndex.get(identifier) ?? [],
|
|
217
|
+
),
|
|
218
|
+
...(obstacle.offBoardConnectsTo ?? []).flatMap(
|
|
219
|
+
(identifier) => rootConnectionIndex.get(identifier) ?? [],
|
|
220
|
+
),
|
|
221
|
+
])
|
|
222
|
+
const rootLabel = rootNames.join(", ")
|
|
223
|
+
return obstacle.layers
|
|
224
|
+
.map((layerName) =>
|
|
225
|
+
rootLabel ? `${layerName}\n${rootLabel}` : layerName,
|
|
226
|
+
)
|
|
227
|
+
.join("\n")
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function visualizeSimpleRouteJson(srj: SimpleRouteJson): GraphicsObject {
|
|
232
|
+
const layerNames = getCopperLayerNames(srj.layerCount)
|
|
233
|
+
const hasArbitraryCopperLayer = (srj.traces ?? []).some((trace) =>
|
|
234
|
+
trace.route.some((routePoint, routePointIndex) => {
|
|
235
|
+
const nextRoutePoint = trace.route[routePointIndex + 1]
|
|
236
|
+
return (
|
|
237
|
+
routePoint.route_type === "wire" &&
|
|
238
|
+
nextRoutePoint?.route_type === "wire" &&
|
|
239
|
+
nextRoutePoint.layer === routePoint.layer &&
|
|
240
|
+
!LEGACY_VISUALIZATION_LAYERS.has(routePoint.layer)
|
|
241
|
+
)
|
|
242
|
+
}),
|
|
243
|
+
)
|
|
244
|
+
const connectionNames = new Set(srj.connections.map(({ name }) => name))
|
|
245
|
+
const traceOnlyConnections: SimpleRouteJson["connections"] =
|
|
246
|
+
hasArbitraryCopperLayer
|
|
247
|
+
? [
|
|
248
|
+
...new Set(
|
|
249
|
+
(srj.traces ?? [])
|
|
250
|
+
.map(({ connection_name }) => connection_name)
|
|
251
|
+
.filter(
|
|
252
|
+
(connectionName) =>
|
|
253
|
+
connectionName && !connectionNames.has(connectionName),
|
|
254
|
+
),
|
|
255
|
+
),
|
|
256
|
+
].map((name) => ({ name, pointsToConnect: [] }))
|
|
257
|
+
: []
|
|
258
|
+
const visualizedConnections = [...srj.connections, ...traceOnlyConnections]
|
|
259
|
+
const colorMap = getColorMap(visualizedConnections)
|
|
260
|
+
const formatObstacleLabel = createObstacleLabelFormatter({
|
|
261
|
+
...srj,
|
|
262
|
+
connections: visualizedConnections,
|
|
263
|
+
})
|
|
264
|
+
const lines: Line[] = []
|
|
265
|
+
const circles: Circle[] = []
|
|
266
|
+
const rects: Rect[] = []
|
|
267
|
+
const points: Point[] = []
|
|
268
|
+
|
|
269
|
+
for (const connection of visualizedConnections) {
|
|
270
|
+
for (const point of connection.pointsToConnect) {
|
|
271
|
+
const pointLayers = getPointLayers(point)
|
|
272
|
+
const rootNames = connection.__rootConnectionNames ?? [connection.name]
|
|
273
|
+
points.push({
|
|
274
|
+
x: point.x,
|
|
275
|
+
y: point.y,
|
|
276
|
+
color: colorMap[connection.name]!,
|
|
277
|
+
layer: getGraphicsLayer(layerNames, pointLayers),
|
|
278
|
+
label: [
|
|
279
|
+
connection.name,
|
|
280
|
+
rootNames.join(", "),
|
|
281
|
+
pointLayers.join(","),
|
|
282
|
+
].join("\n"),
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (const trace of srj.traces ?? []) {
|
|
288
|
+
const jumpers = trace.route.filter(
|
|
289
|
+
(routePoint) => routePoint.route_type === "jumper",
|
|
290
|
+
)
|
|
291
|
+
const isWireSegmentInsideJumper = (
|
|
292
|
+
start: { x: number; y: number },
|
|
293
|
+
end: { x: number; y: number },
|
|
294
|
+
): boolean =>
|
|
295
|
+
jumpers.some((jumper) => {
|
|
296
|
+
const tolerance = 0.01
|
|
297
|
+
return (
|
|
298
|
+
(Math.abs(start.x - jumper.start.x) < tolerance &&
|
|
299
|
+
Math.abs(start.y - jumper.start.y) < tolerance &&
|
|
300
|
+
Math.abs(end.x - jumper.end.x) < tolerance &&
|
|
301
|
+
Math.abs(end.y - jumper.end.y) < tolerance) ||
|
|
302
|
+
(Math.abs(start.x - jumper.end.x) < tolerance &&
|
|
303
|
+
Math.abs(start.y - jumper.end.y) < tolerance &&
|
|
304
|
+
Math.abs(end.x - jumper.start.x) < tolerance &&
|
|
305
|
+
Math.abs(end.y - jumper.start.y) < tolerance)
|
|
306
|
+
)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
for (const routePoint of trace.route) {
|
|
310
|
+
if (routePoint.route_type === "via") {
|
|
311
|
+
const viaLayers = getViaLayerNames(
|
|
312
|
+
layerNames,
|
|
313
|
+
routePoint.from_layer,
|
|
314
|
+
routePoint.to_layer,
|
|
315
|
+
)
|
|
316
|
+
circles.push({
|
|
317
|
+
center: { x: routePoint.x, y: routePoint.y },
|
|
318
|
+
radius: (routePoint.via_diameter ?? getViaPadDiameter(srj)) / 2,
|
|
319
|
+
fill: hasArbitraryCopperLayer
|
|
320
|
+
? colorMap[trace.connection_name]!
|
|
321
|
+
: "blue",
|
|
322
|
+
stroke: "none",
|
|
323
|
+
layer: getGraphicsLayer(layerNames, viaLayers),
|
|
324
|
+
})
|
|
325
|
+
} else if (routePoint.route_type === "through_obstacle") {
|
|
326
|
+
lines.push({
|
|
327
|
+
points: [routePoint.start, routePoint.end],
|
|
328
|
+
strokeColor: transparentize(
|
|
329
|
+
colorMap[trace.connection_name] ?? "purple",
|
|
330
|
+
0.35,
|
|
331
|
+
),
|
|
332
|
+
strokeWidth: routePoint.width,
|
|
333
|
+
strokeDash: [0.1, 0.1],
|
|
334
|
+
layer: getGraphicsLayer(layerNames, [
|
|
335
|
+
routePoint.from_layer,
|
|
336
|
+
routePoint.to_layer,
|
|
337
|
+
]),
|
|
338
|
+
label: `${trace.connection_name} through_obstacle`,
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (
|
|
344
|
+
let routePointIndex = 0;
|
|
345
|
+
routePointIndex < trace.route.length - 1;
|
|
346
|
+
routePointIndex++
|
|
347
|
+
) {
|
|
348
|
+
const routePoint = trace.route[routePointIndex]!
|
|
349
|
+
const nextRoutePoint = trace.route[routePointIndex + 1]!
|
|
350
|
+
if (routePoint.route_type === "jumper") {
|
|
351
|
+
const color =
|
|
352
|
+
colorMap[trace.connection_name] ?? "rgba(255, 165, 0, 0.8)"
|
|
353
|
+
const dimensions =
|
|
354
|
+
JUMPER_DIMENSIONS[
|
|
355
|
+
routePoint.footprint === "1206x4_pair" ? "1206x4_pair" : "0603"
|
|
356
|
+
]
|
|
357
|
+
const horizontal =
|
|
358
|
+
Math.abs(routePoint.end.x - routePoint.start.x) >
|
|
359
|
+
Math.abs(routePoint.end.y - routePoint.start.y)
|
|
360
|
+
const padWidth = horizontal ? dimensions.padLength : dimensions.padWidth
|
|
361
|
+
const padHeight = horizontal
|
|
362
|
+
? dimensions.padWidth
|
|
363
|
+
: dimensions.padLength
|
|
364
|
+
const layer = getGraphicsLayer(layerNames, [routePoint.layer])
|
|
365
|
+
for (const center of [routePoint.start, routePoint.end]) {
|
|
366
|
+
rects.push({
|
|
367
|
+
center,
|
|
368
|
+
width: padWidth,
|
|
369
|
+
height: padHeight,
|
|
370
|
+
fill: transparentize(color, 0.5),
|
|
371
|
+
stroke: "rgba(0, 0, 0, 0.5)",
|
|
372
|
+
layer,
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
lines.push({
|
|
376
|
+
points: [routePoint.start, routePoint.end],
|
|
377
|
+
strokeColor: "rgba(100, 100, 100, 0.8)",
|
|
378
|
+
strokeWidth: dimensions.padWidth * 0.3,
|
|
379
|
+
layer,
|
|
380
|
+
})
|
|
381
|
+
} else if (
|
|
382
|
+
routePoint.route_type === "wire" &&
|
|
383
|
+
nextRoutePoint.route_type === "wire" &&
|
|
384
|
+
nextRoutePoint.layer === routePoint.layer &&
|
|
385
|
+
!isWireSegmentInsideJumper(routePoint, nextRoutePoint)
|
|
386
|
+
) {
|
|
387
|
+
const layerIndex = getLayerIndex(layerNames, routePoint.layer)
|
|
388
|
+
lines.push({
|
|
389
|
+
points: [
|
|
390
|
+
{ x: routePoint.x, y: routePoint.y },
|
|
391
|
+
{ x: nextRoutePoint.x, y: nextRoutePoint.y },
|
|
392
|
+
],
|
|
393
|
+
layer: `z${layerIndex}`,
|
|
394
|
+
strokeWidth: routePoint.width,
|
|
395
|
+
strokeColor: getCopperLayerColor(layerIndex),
|
|
396
|
+
...(hasArbitraryCopperLayer ? { label: trace.connection_name } : {}),
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const obstacle of srj.obstacles) {
|
|
403
|
+
if (obstacle.isCopperPour) continue
|
|
404
|
+
const layerIndexes = getObstacleLayerIndexes(obstacle, layerNames)
|
|
405
|
+
if (layerIndexes.length === 0) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`FanoutSolver: cannot visualize obstacle "${obstacle.obstacleId ?? "unknown"}" without a valid layer`,
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
const onlyLayerName =
|
|
411
|
+
layerIndexes.length === 1 ? layerNames[layerIndexes[0]!] : undefined
|
|
412
|
+
const fill = transparentize(
|
|
413
|
+
onlyLayerName === "bottom" ? "blue" : "red",
|
|
414
|
+
0.5 ** layerIndexes.length,
|
|
415
|
+
)
|
|
416
|
+
const shape = (obstacle as Obstacle & { shape?: string }).shape
|
|
417
|
+
const common = {
|
|
418
|
+
center: obstacle.center,
|
|
419
|
+
fill,
|
|
420
|
+
layer: `z${layerIndexes.join(",")}`,
|
|
421
|
+
label: formatObstacleLabel(obstacle),
|
|
422
|
+
}
|
|
423
|
+
if (shape === "circle") {
|
|
424
|
+
circles.push({
|
|
425
|
+
...common,
|
|
426
|
+
radius: Math.min(obstacle.width, obstacle.height) / 2,
|
|
427
|
+
})
|
|
428
|
+
} else {
|
|
429
|
+
rects.push({
|
|
430
|
+
...common,
|
|
431
|
+
width: obstacle.width,
|
|
432
|
+
height: obstacle.height,
|
|
433
|
+
ccwRotationDegrees: obstacle.ccwRotationDegrees,
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const jumper of srj.jumpers ?? []) {
|
|
439
|
+
for (const pad of jumper.pads) {
|
|
440
|
+
rects.push({
|
|
441
|
+
center: pad.center,
|
|
442
|
+
width: pad.width,
|
|
443
|
+
height: pad.height,
|
|
444
|
+
ccwRotationDegrees: pad.ccwRotationDegrees,
|
|
445
|
+
fill: "rgba(255, 165, 0, 0.3)",
|
|
446
|
+
stroke: "rgba(255, 165, 0, 0.8)",
|
|
447
|
+
layer: getGraphicsLayer(layerNames, pad.layers),
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return { rects, circles, lines, points }
|
|
453
|
+
}
|