@tscircuit/fanout-solver 0.0.10
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/LICENSE +21 -0
- package/README.md +366 -0
- package/lib/build-output.ts +140 -0
- package/lib/fanout-solver.ts +617 -0
- package/lib/geometry.ts +162 -0
- package/lib/index.ts +20 -0
- package/lib/layer-colors.ts +21 -0
- package/lib/layer-names.ts +137 -0
- package/lib/prepare-buses.ts +1016 -0
- package/lib/route-bus.ts +934 -0
- package/lib/route-single-layer-adaptive-exits.ts +1179 -0
- package/lib/route-single-layer-push-shove.ts +1233 -0
- package/lib/types.ts +212 -0
- package/package.json +56 -0
package/lib/route-bus.ts
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Obstacle,
|
|
3
|
+
SimpleRouteJson,
|
|
4
|
+
SimplifiedPcbTrace,
|
|
5
|
+
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import {
|
|
7
|
+
distance,
|
|
8
|
+
distancePointToObstacle,
|
|
9
|
+
distancePointToSegment,
|
|
10
|
+
distanceSegmentToObstacle,
|
|
11
|
+
segmentsAreClear,
|
|
12
|
+
} from "./geometry"
|
|
13
|
+
import { getLayerSpan } from "./layer-names"
|
|
14
|
+
import type {
|
|
15
|
+
FanoutDirection,
|
|
16
|
+
FanoutRoutePlan,
|
|
17
|
+
Point2D,
|
|
18
|
+
PreparedBus,
|
|
19
|
+
PreparedConnection,
|
|
20
|
+
RoutedSegment,
|
|
21
|
+
} from "./types"
|
|
22
|
+
|
|
23
|
+
interface RouteBusParams {
|
|
24
|
+
srj: SimpleRouteJson
|
|
25
|
+
bus: PreparedBus
|
|
26
|
+
targetLayer: string
|
|
27
|
+
acceptedPlans: FanoutRoutePlan[]
|
|
28
|
+
layerNames: string[]
|
|
29
|
+
traceWidth: number
|
|
30
|
+
viaDiameter: number
|
|
31
|
+
viaHoleDiameter: number
|
|
32
|
+
clearance: number
|
|
33
|
+
compactBusTracks: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface TrackCandidate {
|
|
37
|
+
value: number
|
|
38
|
+
kind: "corridor" | "gap" | "margin" | "preferred"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ViaHandedness = -1 | 0 | 1
|
|
42
|
+
|
|
43
|
+
function isHorizontal(direction: FanoutDirection): boolean {
|
|
44
|
+
return direction === "left" || direction === "right"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function directionSign(direction: FanoutDirection): number {
|
|
48
|
+
return direction === "right" || direction === "up" ? 1 : -1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getAxis(point: Point2D, direction: FanoutDirection): number {
|
|
52
|
+
return isHorizontal(direction) ? point.x : point.y
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getPerpendicularAxis(
|
|
56
|
+
point: Point2D,
|
|
57
|
+
direction: FanoutDirection,
|
|
58
|
+
): number {
|
|
59
|
+
return isHorizontal(direction) ? point.y : point.x
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makePoint(
|
|
63
|
+
axis: number,
|
|
64
|
+
perpendicularAxis: number,
|
|
65
|
+
direction: FanoutDirection,
|
|
66
|
+
): Point2D {
|
|
67
|
+
return isHorizontal(direction)
|
|
68
|
+
? { x: axis, y: perpendicularAxis }
|
|
69
|
+
: { x: perpendicularAxis, y: axis }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getExitAxis(bus: PreparedBus): number {
|
|
73
|
+
switch (bus.direction) {
|
|
74
|
+
case "right":
|
|
75
|
+
return bus.sharedBoundary.maxX
|
|
76
|
+
case "left":
|
|
77
|
+
return bus.sharedBoundary.minX
|
|
78
|
+
case "up":
|
|
79
|
+
return bus.sharedBoundary.maxY
|
|
80
|
+
case "down":
|
|
81
|
+
return bus.sharedBoundary.minY
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getDirectionalPitch(bus: PreparedBus): number {
|
|
86
|
+
return isHorizontal(bus.direction) ? bus.pitchX : bus.pitchY
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getPerpendicularPitch(bus: PreparedBus): number {
|
|
90
|
+
return isHorizontal(bus.direction) ? bus.pitchY : bus.pitchX
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getDepthInRows(bus: PreparedBus): number {
|
|
94
|
+
const directionalCoordinates = (
|
|
95
|
+
isHorizontal(bus.direction) ? bus.xCoordinates : bus.yCoordinates
|
|
96
|
+
).toSorted((a, b) => a - b)
|
|
97
|
+
const averageDirectionalSource =
|
|
98
|
+
bus.connections.reduce(
|
|
99
|
+
(sum, candidate) => sum + getAxis(candidate.sourcePoint, bus.direction),
|
|
100
|
+
0,
|
|
101
|
+
) / bus.connections.length
|
|
102
|
+
const outwardCoordinate =
|
|
103
|
+
directionSign(bus.direction) > 0
|
|
104
|
+
? directionalCoordinates.at(-1)!
|
|
105
|
+
: directionalCoordinates[0]!
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
Math.abs(outwardCoordinate - averageDirectionalSource) /
|
|
109
|
+
getDirectionalPitch(bus)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function busIsOnOutwardComponentEdge(bus: PreparedBus): boolean {
|
|
114
|
+
const directionalCoordinates = isHorizontal(bus.direction)
|
|
115
|
+
? bus.xCoordinates
|
|
116
|
+
: bus.yCoordinates
|
|
117
|
+
const averageDirectionalSource =
|
|
118
|
+
bus.connections.reduce(
|
|
119
|
+
(sum, connection) => sum + getAxis(connection.sourcePoint, bus.direction),
|
|
120
|
+
0,
|
|
121
|
+
) / bus.connections.length
|
|
122
|
+
const outwardCoordinate =
|
|
123
|
+
directionSign(bus.direction) > 0
|
|
124
|
+
? Math.max(...directionalCoordinates)
|
|
125
|
+
: Math.min(...directionalCoordinates)
|
|
126
|
+
return Math.abs(averageDirectionalSource - outwardCoordinate) < 1e-6
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getConnectionRank(
|
|
130
|
+
bus: PreparedBus,
|
|
131
|
+
connection: PreparedConnection,
|
|
132
|
+
): number {
|
|
133
|
+
const connectionRank = [...bus.connections]
|
|
134
|
+
.sort(
|
|
135
|
+
(a, b) =>
|
|
136
|
+
getPerpendicularAxis(a.sourcePoint, bus.direction) -
|
|
137
|
+
getPerpendicularAxis(b.sourcePoint, bus.direction),
|
|
138
|
+
)
|
|
139
|
+
.findIndex(
|
|
140
|
+
(candidate) => candidate.connectionIndex === connection.connectionIndex,
|
|
141
|
+
)
|
|
142
|
+
if (connectionRank < 0) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`FanoutSolver: connection "${connection.connection.name}" is missing from bus "${bus.busId}"`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return connectionRank
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function pointIsInsideBounds(
|
|
151
|
+
point: Point2D,
|
|
152
|
+
bounds: SimpleRouteJson["bounds"],
|
|
153
|
+
): boolean {
|
|
154
|
+
return (
|
|
155
|
+
point.x >= bounds.minX - 1e-6 &&
|
|
156
|
+
point.x <= bounds.maxX + 1e-6 &&
|
|
157
|
+
point.y >= bounds.minY - 1e-6 &&
|
|
158
|
+
point.y <= bounds.maxY + 1e-6
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getTracksInSpan(
|
|
163
|
+
minimum: number,
|
|
164
|
+
maximum: number,
|
|
165
|
+
traceWidth: number,
|
|
166
|
+
clearance: number,
|
|
167
|
+
kind: TrackCandidate["kind"],
|
|
168
|
+
): TrackCandidate[] {
|
|
169
|
+
const freeWidth = maximum - minimum
|
|
170
|
+
const trackCount = Math.floor(
|
|
171
|
+
(freeWidth - clearance) / (traceWidth + clearance) + 1e-9,
|
|
172
|
+
)
|
|
173
|
+
if (trackCount < 1) return []
|
|
174
|
+
const usedWidth = trackCount * traceWidth + (trackCount - 1) * clearance
|
|
175
|
+
const firstTrack = minimum + (freeWidth - usedWidth) / 2 + traceWidth / 2
|
|
176
|
+
return Array.from({ length: trackCount }, (_, index) => ({
|
|
177
|
+
value: firstTrack + index * (traceWidth + clearance),
|
|
178
|
+
kind,
|
|
179
|
+
}))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getTrackCandidates(params: {
|
|
183
|
+
bus: PreparedBus
|
|
184
|
+
connection: PreparedConnection
|
|
185
|
+
preferredTrack: number
|
|
186
|
+
traceWidth: number
|
|
187
|
+
clearance: number
|
|
188
|
+
}): TrackCandidate[] {
|
|
189
|
+
const { bus, connection, preferredTrack, traceWidth, clearance } = params
|
|
190
|
+
const coordinates = (
|
|
191
|
+
isHorizontal(bus.direction) ? bus.yCoordinates : bus.xCoordinates
|
|
192
|
+
).toSorted((a, b) => a - b)
|
|
193
|
+
const obstacleHalfSize = Math.max(
|
|
194
|
+
...bus.componentObstacles.map((obstacle) =>
|
|
195
|
+
isHorizontal(bus.direction) ? obstacle.height / 2 : obstacle.width / 2,
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
const boundaryMinimum = isHorizontal(bus.direction)
|
|
199
|
+
? bus.sharedBoundary.minY
|
|
200
|
+
: bus.sharedBoundary.minX
|
|
201
|
+
const boundaryMaximum = isHorizontal(bus.direction)
|
|
202
|
+
? bus.sharedBoundary.maxY
|
|
203
|
+
: bus.sharedBoundary.maxX
|
|
204
|
+
const maximumJog = boundaryMaximum - boundaryMinimum
|
|
205
|
+
const ladderMinimum = boundaryMinimum
|
|
206
|
+
const ladderMaximum = boundaryMaximum
|
|
207
|
+
const tracks: TrackCandidate[] = [
|
|
208
|
+
{ value: preferredTrack, kind: "preferred" },
|
|
209
|
+
...getTracksInSpan(
|
|
210
|
+
ladderMinimum,
|
|
211
|
+
coordinates[0]! - obstacleHalfSize,
|
|
212
|
+
traceWidth,
|
|
213
|
+
clearance,
|
|
214
|
+
"margin",
|
|
215
|
+
),
|
|
216
|
+
]
|
|
217
|
+
for (let index = 0; index < coordinates.length; index++) {
|
|
218
|
+
tracks.push({ value: coordinates[index]!, kind: "corridor" })
|
|
219
|
+
if (index < coordinates.length - 1) {
|
|
220
|
+
tracks.push(
|
|
221
|
+
...getTracksInSpan(
|
|
222
|
+
coordinates[index]! + obstacleHalfSize,
|
|
223
|
+
coordinates[index + 1]! - obstacleHalfSize,
|
|
224
|
+
traceWidth,
|
|
225
|
+
clearance,
|
|
226
|
+
"gap",
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
tracks.push(
|
|
232
|
+
...getTracksInSpan(
|
|
233
|
+
coordinates.at(-1)! + obstacleHalfSize,
|
|
234
|
+
ladderMaximum,
|
|
235
|
+
traceWidth,
|
|
236
|
+
clearance,
|
|
237
|
+
"margin",
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const sourceTrack = getPerpendicularAxis(
|
|
242
|
+
connection.sourcePoint,
|
|
243
|
+
bus.direction,
|
|
244
|
+
)
|
|
245
|
+
const componentCenter = (coordinates[0]! + coordinates.at(-1)!) / 2
|
|
246
|
+
return tracks
|
|
247
|
+
.filter((track) => Math.abs(track.value - sourceTrack) <= maximumJog + 1e-9)
|
|
248
|
+
.filter(
|
|
249
|
+
(track, index, candidates) =>
|
|
250
|
+
candidates.findIndex(
|
|
251
|
+
(candidate) => Math.abs(candidate.value - track.value) < 1e-9,
|
|
252
|
+
) === index,
|
|
253
|
+
)
|
|
254
|
+
.sort(
|
|
255
|
+
(a, b) =>
|
|
256
|
+
Math.abs(a.value - preferredTrack) -
|
|
257
|
+
Math.abs(b.value - preferredTrack) -
|
|
258
|
+
(Math.abs(a.value - componentCenter) -
|
|
259
|
+
Math.abs(b.value - componentCenter)) *
|
|
260
|
+
1e-3,
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getPreferredTrack(params: {
|
|
265
|
+
bus: PreparedBus
|
|
266
|
+
connection: PreparedConnection
|
|
267
|
+
targetUsesVia: boolean
|
|
268
|
+
interstitialEscape: boolean
|
|
269
|
+
compactBusTracks: boolean
|
|
270
|
+
traceWidth: number
|
|
271
|
+
viaDiameter: number
|
|
272
|
+
clearance: number
|
|
273
|
+
}): number {
|
|
274
|
+
const {
|
|
275
|
+
bus,
|
|
276
|
+
connection,
|
|
277
|
+
targetUsesVia,
|
|
278
|
+
interstitialEscape,
|
|
279
|
+
compactBusTracks,
|
|
280
|
+
traceWidth,
|
|
281
|
+
viaDiameter,
|
|
282
|
+
clearance,
|
|
283
|
+
} = params
|
|
284
|
+
const perpendicularCoordinates = (
|
|
285
|
+
isHorizontal(bus.direction) ? bus.yCoordinates : bus.xCoordinates
|
|
286
|
+
).toSorted((a, b) => a - b)
|
|
287
|
+
const sourceTrack = getPerpendicularAxis(
|
|
288
|
+
connection.sourcePoint,
|
|
289
|
+
bus.direction,
|
|
290
|
+
)
|
|
291
|
+
if (compactBusTracks) {
|
|
292
|
+
const connectionRank = getConnectionRank(bus, connection)
|
|
293
|
+
const componentCenter =
|
|
294
|
+
(perpendicularCoordinates[0]! + perpendicularCoordinates.at(-1)!) / 2
|
|
295
|
+
return (
|
|
296
|
+
componentCenter +
|
|
297
|
+
(connectionRank - (bus.connections.length - 1) / 2) *
|
|
298
|
+
(traceWidth + clearance)
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
if (!targetUsesVia) return sourceTrack
|
|
302
|
+
if (!interstitialEscape) return sourceTrack
|
|
303
|
+
|
|
304
|
+
const depthInRows = getDepthInRows(bus)
|
|
305
|
+
|
|
306
|
+
const trackPitch = traceWidth + clearance
|
|
307
|
+
const halfConnectionCount = Math.ceil(bus.connections.length / 2)
|
|
308
|
+
const sideBandWidth = (halfConnectionCount - 1) * trackPitch
|
|
309
|
+
const bandSeparation = viaDiameter / 2 + traceWidth / 2 + clearance + 1e-3
|
|
310
|
+
const depthIndex = Math.round(depthInRows) + 1
|
|
311
|
+
const nearOffset =
|
|
312
|
+
depthIndex * bandSeparation + (depthIndex - 1) * sideBandWidth
|
|
313
|
+
const connectionRank = getConnectionRank(bus, connection)
|
|
314
|
+
const componentMinimum = perpendicularCoordinates[0]!
|
|
315
|
+
const componentMaximum = perpendicularCoordinates.at(-1)!
|
|
316
|
+
const requestedTrack =
|
|
317
|
+
connectionRank < halfConnectionCount
|
|
318
|
+
? componentMinimum -
|
|
319
|
+
nearOffset -
|
|
320
|
+
(halfConnectionCount - connectionRank - 1) * trackPitch
|
|
321
|
+
: componentMaximum +
|
|
322
|
+
nearOffset +
|
|
323
|
+
(connectionRank - halfConnectionCount) * trackPitch
|
|
324
|
+
const boundaryMinimum = isHorizontal(bus.direction)
|
|
325
|
+
? bus.sharedBoundary.minY
|
|
326
|
+
: bus.sharedBoundary.minX
|
|
327
|
+
const boundaryMaximum = isHorizontal(bus.direction)
|
|
328
|
+
? bus.sharedBoundary.maxY
|
|
329
|
+
: bus.sharedBoundary.maxX
|
|
330
|
+
|
|
331
|
+
return Math.max(
|
|
332
|
+
boundaryMinimum + traceWidth / 2,
|
|
333
|
+
Math.min(boundaryMaximum - traceWidth / 2, requestedTrack),
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getConnectionOrders(bus: PreparedBus): PreparedConnection[][] {
|
|
338
|
+
const sign = directionSign(bus.direction)
|
|
339
|
+
const outwardFirst = [...bus.connections].sort((a, b) => {
|
|
340
|
+
const directionalDifference =
|
|
341
|
+
sign *
|
|
342
|
+
(getAxis(b.sourcePoint, bus.direction) -
|
|
343
|
+
getAxis(a.sourcePoint, bus.direction))
|
|
344
|
+
if (Math.abs(directionalDifference) > 1e-6) {
|
|
345
|
+
return directionalDifference
|
|
346
|
+
}
|
|
347
|
+
return (
|
|
348
|
+
getPerpendicularAxis(a.sourcePoint, bus.direction) -
|
|
349
|
+
getPerpendicularAxis(b.sourcePoint, bus.direction)
|
|
350
|
+
)
|
|
351
|
+
})
|
|
352
|
+
const perpendicularFirst = [...bus.connections].sort(
|
|
353
|
+
(a, b) =>
|
|
354
|
+
getPerpendicularAxis(a.sourcePoint, bus.direction) -
|
|
355
|
+
getPerpendicularAxis(b.sourcePoint, bus.direction) ||
|
|
356
|
+
sign *
|
|
357
|
+
(getAxis(b.sourcePoint, bus.direction) -
|
|
358
|
+
getAxis(a.sourcePoint, bus.direction)),
|
|
359
|
+
)
|
|
360
|
+
const orders = [
|
|
361
|
+
outwardFirst,
|
|
362
|
+
[...outwardFirst].reverse(),
|
|
363
|
+
perpendicularFirst,
|
|
364
|
+
[...perpendicularFirst].reverse(),
|
|
365
|
+
]
|
|
366
|
+
for (let offset = 1; offset < Math.min(outwardFirst.length, 8); offset++) {
|
|
367
|
+
orders.push([
|
|
368
|
+
...outwardFirst.slice(offset),
|
|
369
|
+
...outwardFirst.slice(0, offset),
|
|
370
|
+
])
|
|
371
|
+
}
|
|
372
|
+
return orders
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function appendSegment(
|
|
376
|
+
segments: RoutedSegment[],
|
|
377
|
+
start: Point2D,
|
|
378
|
+
end: Point2D,
|
|
379
|
+
width: number,
|
|
380
|
+
layer: string,
|
|
381
|
+
): void {
|
|
382
|
+
if (distance(start, end) < 1e-9) return
|
|
383
|
+
segments.push({ start, end, width, layer })
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function chamferOrthogonalPolyline(
|
|
387
|
+
points: Point2D[],
|
|
388
|
+
requestedChamfer: number,
|
|
389
|
+
): Point2D[] {
|
|
390
|
+
if (points.length < 3) return points
|
|
391
|
+
const chamfered: Point2D[] = [points[0]!]
|
|
392
|
+
|
|
393
|
+
for (let index = 1; index < points.length - 1; index++) {
|
|
394
|
+
const previous = points[index - 1]!
|
|
395
|
+
const corner = points[index]!
|
|
396
|
+
const next = points[index + 1]!
|
|
397
|
+
const incomingLength = distance(previous, corner)
|
|
398
|
+
const outgoingLength = distance(corner, next)
|
|
399
|
+
if (incomingLength < 1e-9 || outgoingLength < 1e-9) continue
|
|
400
|
+
const incomingUnit = {
|
|
401
|
+
x: (corner.x - previous.x) / incomingLength,
|
|
402
|
+
y: (corner.y - previous.y) / incomingLength,
|
|
403
|
+
}
|
|
404
|
+
const outgoingUnit = {
|
|
405
|
+
x: (next.x - corner.x) / outgoingLength,
|
|
406
|
+
y: (next.y - corner.y) / outgoingLength,
|
|
407
|
+
}
|
|
408
|
+
const dot =
|
|
409
|
+
incomingUnit.x * outgoingUnit.x + incomingUnit.y * outgoingUnit.y
|
|
410
|
+
if (Math.abs(dot) > 1e-6) {
|
|
411
|
+
chamfered.push(corner)
|
|
412
|
+
continue
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const chamfer = Math.min(
|
|
416
|
+
requestedChamfer,
|
|
417
|
+
incomingLength / 2,
|
|
418
|
+
outgoingLength / 2,
|
|
419
|
+
)
|
|
420
|
+
chamfered.push({
|
|
421
|
+
x: corner.x - incomingUnit.x * chamfer,
|
|
422
|
+
y: corner.y - incomingUnit.y * chamfer,
|
|
423
|
+
})
|
|
424
|
+
chamfered.push({
|
|
425
|
+
x: corner.x + outgoingUnit.x * chamfer,
|
|
426
|
+
y: corner.y + outgoingUnit.y * chamfer,
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
chamfered.push(points.at(-1)!)
|
|
431
|
+
return chamfered
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function buildPlan(params: {
|
|
435
|
+
preparedConnection: PreparedConnection
|
|
436
|
+
bus: PreparedBus
|
|
437
|
+
targetLayer: string
|
|
438
|
+
track: number
|
|
439
|
+
exitAxis: number
|
|
440
|
+
layerNames: string[]
|
|
441
|
+
traceWidth: number
|
|
442
|
+
viaDiameter: number
|
|
443
|
+
viaHoleDiameter: number
|
|
444
|
+
viaHandedness: ViaHandedness
|
|
445
|
+
interstitialEscape: boolean
|
|
446
|
+
spreadLaneIndex: number
|
|
447
|
+
clearance: number
|
|
448
|
+
terminateAtVia: boolean
|
|
449
|
+
}): FanoutRoutePlan {
|
|
450
|
+
const {
|
|
451
|
+
preparedConnection,
|
|
452
|
+
bus,
|
|
453
|
+
targetLayer,
|
|
454
|
+
track,
|
|
455
|
+
exitAxis,
|
|
456
|
+
layerNames,
|
|
457
|
+
traceWidth,
|
|
458
|
+
viaDiameter,
|
|
459
|
+
viaHoleDiameter,
|
|
460
|
+
viaHandedness,
|
|
461
|
+
interstitialEscape,
|
|
462
|
+
spreadLaneIndex,
|
|
463
|
+
clearance,
|
|
464
|
+
terminateAtVia,
|
|
465
|
+
} = params
|
|
466
|
+
const sourcePoint = {
|
|
467
|
+
x: preparedConnection.sourcePoint.x,
|
|
468
|
+
y: preparedConnection.sourcePoint.y,
|
|
469
|
+
}
|
|
470
|
+
const sign = directionSign(bus.direction)
|
|
471
|
+
const directionalPitch = getDirectionalPitch(bus)
|
|
472
|
+
const perpendicularPitch = getPerpendicularPitch(bus)
|
|
473
|
+
const targetUsesVia = targetLayer !== preparedConnection.sourceLayer
|
|
474
|
+
const directionalPadSize = isHorizontal(bus.direction)
|
|
475
|
+
? preparedConnection.sourceObstacle.width
|
|
476
|
+
: preparedConnection.sourceObstacle.height
|
|
477
|
+
const initialEscapeDistance =
|
|
478
|
+
targetUsesVia && !busIsOnOutwardComponentEdge(bus)
|
|
479
|
+
? directionalPadSize >= directionalPitch
|
|
480
|
+
? directionalPadSize / 2 + viaDiameter / 2 + clearance + 1e-3
|
|
481
|
+
: directionalPitch * 0.5
|
|
482
|
+
: !targetUsesVia
|
|
483
|
+
? directionalPadSize / 2 + traceWidth / 2 + clearance + 1e-3
|
|
484
|
+
: Math.max(
|
|
485
|
+
directionalPitch * 0.5,
|
|
486
|
+
directionalPadSize / 2 +
|
|
487
|
+
(targetUsesVia ? viaDiameter : traceWidth) / 2 +
|
|
488
|
+
clearance +
|
|
489
|
+
1e-3,
|
|
490
|
+
)
|
|
491
|
+
const viaAxis =
|
|
492
|
+
getAxis(sourcePoint, bus.direction) + sign * initialEscapeDistance
|
|
493
|
+
const sourcePerpendicularAxis = getPerpendicularAxis(
|
|
494
|
+
sourcePoint,
|
|
495
|
+
bus.direction,
|
|
496
|
+
)
|
|
497
|
+
const viaPerpendicularAxis =
|
|
498
|
+
sourcePerpendicularAxis + viaHandedness * perpendicularPitch * 0.5
|
|
499
|
+
const viaPoint = makePoint(viaAxis, viaPerpendicularAxis, bus.direction)
|
|
500
|
+
const spreadLaneDistance =
|
|
501
|
+
viaDiameter / 2 +
|
|
502
|
+
traceWidth / 2 +
|
|
503
|
+
clearance +
|
|
504
|
+
1e-3 +
|
|
505
|
+
spreadLaneIndex * (traceWidth + clearance)
|
|
506
|
+
const useNestedSpread =
|
|
507
|
+
interstitialEscape &&
|
|
508
|
+
directionalPitch >= spreadLaneDistance + viaDiameter / 2 + clearance
|
|
509
|
+
const spreadPoint = useNestedSpread
|
|
510
|
+
? makePoint(
|
|
511
|
+
viaAxis + sign * spreadLaneDistance,
|
|
512
|
+
viaPerpendicularAxis,
|
|
513
|
+
bus.direction,
|
|
514
|
+
)
|
|
515
|
+
: viaPoint
|
|
516
|
+
const targetLayerDoglegAxis = useNestedSpread
|
|
517
|
+
? getAxis(spreadPoint, bus.direction)
|
|
518
|
+
: viaAxis + sign * Math.abs(track - viaPerpendicularAxis)
|
|
519
|
+
const doglegPoint = makePoint(targetLayerDoglegAxis, track, bus.direction)
|
|
520
|
+
const exitPoint = terminateAtVia
|
|
521
|
+
? viaPoint
|
|
522
|
+
: makePoint(exitAxis, track, bus.direction)
|
|
523
|
+
const segments: RoutedSegment[] = []
|
|
524
|
+
const route: SimplifiedPcbTrace["route"] = []
|
|
525
|
+
|
|
526
|
+
route.push({
|
|
527
|
+
route_type: "wire",
|
|
528
|
+
x: sourcePoint.x,
|
|
529
|
+
y: sourcePoint.y,
|
|
530
|
+
width: traceWidth,
|
|
531
|
+
layer: preparedConnection.sourceLayer,
|
|
532
|
+
start_pcb_port_id: preparedConnection.sourcePoint.pcb_port_id,
|
|
533
|
+
})
|
|
534
|
+
appendSegment(
|
|
535
|
+
segments,
|
|
536
|
+
sourcePoint,
|
|
537
|
+
viaPoint,
|
|
538
|
+
traceWidth,
|
|
539
|
+
preparedConnection.sourceLayer,
|
|
540
|
+
)
|
|
541
|
+
route.push({
|
|
542
|
+
route_type: "wire",
|
|
543
|
+
x: viaPoint.x,
|
|
544
|
+
y: viaPoint.y,
|
|
545
|
+
width: traceWidth,
|
|
546
|
+
layer: preparedConnection.sourceLayer,
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
let via: FanoutRoutePlan["via"]
|
|
550
|
+
if (targetLayer !== preparedConnection.sourceLayer) {
|
|
551
|
+
const spanLayers = getLayerSpan(
|
|
552
|
+
preparedConnection.sourceLayer,
|
|
553
|
+
targetLayer,
|
|
554
|
+
layerNames,
|
|
555
|
+
)
|
|
556
|
+
via = {
|
|
557
|
+
center: viaPoint,
|
|
558
|
+
diameter: viaDiameter,
|
|
559
|
+
holeDiameter: viaHoleDiameter,
|
|
560
|
+
fromLayer: preparedConnection.sourceLayer,
|
|
561
|
+
toLayer: targetLayer,
|
|
562
|
+
spanLayers,
|
|
563
|
+
}
|
|
564
|
+
route.push({
|
|
565
|
+
route_type: "via",
|
|
566
|
+
x: viaPoint.x,
|
|
567
|
+
y: viaPoint.y,
|
|
568
|
+
from_layer: preparedConnection.sourceLayer,
|
|
569
|
+
to_layer: targetLayer,
|
|
570
|
+
via_diameter: viaDiameter,
|
|
571
|
+
via_hole_diameter: viaHoleDiameter,
|
|
572
|
+
})
|
|
573
|
+
route.push({
|
|
574
|
+
route_type: "wire",
|
|
575
|
+
x: viaPoint.x,
|
|
576
|
+
y: viaPoint.y,
|
|
577
|
+
width: traceWidth,
|
|
578
|
+
layer: targetLayer,
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const targetLayerPoints = terminateAtVia
|
|
583
|
+
? [viaPoint]
|
|
584
|
+
: useNestedSpread
|
|
585
|
+
? chamferOrthogonalPolyline(
|
|
586
|
+
[viaPoint, spreadPoint, doglegPoint, exitPoint],
|
|
587
|
+
Math.max(traceWidth + clearance, traceWidth * 2),
|
|
588
|
+
)
|
|
589
|
+
: [viaPoint, doglegPoint, exitPoint]
|
|
590
|
+
for (let index = 1; index < targetLayerPoints.length; index++) {
|
|
591
|
+
const previousPoint = targetLayerPoints[index - 1]!
|
|
592
|
+
const nextPoint = targetLayerPoints[index]!
|
|
593
|
+
appendSegment(segments, previousPoint, nextPoint, traceWidth, targetLayer)
|
|
594
|
+
route.push({
|
|
595
|
+
route_type: "wire",
|
|
596
|
+
x: nextPoint.x,
|
|
597
|
+
y: nextPoint.y,
|
|
598
|
+
width: traceWidth,
|
|
599
|
+
layer: targetLayer,
|
|
600
|
+
})
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
busId: bus.busId,
|
|
605
|
+
connectionName: preparedConnection.connection.name,
|
|
606
|
+
connectionIndex: preparedConnection.connectionIndex,
|
|
607
|
+
sourcePointIndex: preparedConnection.sourcePointIndex,
|
|
608
|
+
sourcePoint: preparedConnection.sourcePoint,
|
|
609
|
+
sourceObstacle: preparedConnection.sourceObstacle,
|
|
610
|
+
sourceLayer: preparedConnection.sourceLayer,
|
|
611
|
+
targetLayer,
|
|
612
|
+
termination: bus.termination,
|
|
613
|
+
direction: bus.direction,
|
|
614
|
+
exitPoint,
|
|
615
|
+
trace: {
|
|
616
|
+
type: "pcb_trace",
|
|
617
|
+
pcb_trace_id: `fanout:${preparedConnection.connection.name}`,
|
|
618
|
+
connection_name: preparedConnection.connection.name,
|
|
619
|
+
connectsTo: [
|
|
620
|
+
preparedConnection.connection.name,
|
|
621
|
+
...(preparedConnection.sourcePoint.pointId
|
|
622
|
+
? [preparedConnection.sourcePoint.pointId]
|
|
623
|
+
: []),
|
|
624
|
+
...(preparedConnection.sourcePoint.pcb_port_id
|
|
625
|
+
? [preparedConnection.sourcePoint.pcb_port_id]
|
|
626
|
+
: []),
|
|
627
|
+
],
|
|
628
|
+
route,
|
|
629
|
+
},
|
|
630
|
+
segments,
|
|
631
|
+
via,
|
|
632
|
+
length: segments.reduce(
|
|
633
|
+
(total, segment) => total + distance(segment.start, segment.end),
|
|
634
|
+
0,
|
|
635
|
+
),
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function segmentIsClearOfObstacles(params: {
|
|
640
|
+
segment: RoutedSegment
|
|
641
|
+
plan: FanoutRoutePlan
|
|
642
|
+
segmentIndex: number
|
|
643
|
+
obstacles: Obstacle[]
|
|
644
|
+
clearance: number
|
|
645
|
+
}): boolean {
|
|
646
|
+
const { segment, plan, segmentIndex, obstacles, clearance } = params
|
|
647
|
+
for (const obstacle of obstacles) {
|
|
648
|
+
if (!obstacle.layers.includes(segment.layer)) continue
|
|
649
|
+
if (obstacle.connectedTo.includes(plan.connectionName)) continue
|
|
650
|
+
if (
|
|
651
|
+
segmentIndex === 0 &&
|
|
652
|
+
obstacle === plan.sourceObstacle &&
|
|
653
|
+
segment.layer === plan.sourceLayer
|
|
654
|
+
) {
|
|
655
|
+
continue
|
|
656
|
+
}
|
|
657
|
+
if (
|
|
658
|
+
distanceSegmentToObstacle(segment, obstacle) <
|
|
659
|
+
segment.width / 2 + clearance - 1e-9
|
|
660
|
+
) {
|
|
661
|
+
return false
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return true
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function planIsClear(params: {
|
|
668
|
+
plan: FanoutRoutePlan
|
|
669
|
+
otherPlans: FanoutRoutePlan[]
|
|
670
|
+
srj: SimpleRouteJson
|
|
671
|
+
clearance: number
|
|
672
|
+
}): boolean {
|
|
673
|
+
const { plan, otherPlans, srj, clearance } = params
|
|
674
|
+
if (
|
|
675
|
+
!pointIsInsideBounds(plan.exitPoint, srj.bounds) ||
|
|
676
|
+
plan.segments.some(
|
|
677
|
+
(segment) =>
|
|
678
|
+
!pointIsInsideBounds(segment.start, srj.bounds) ||
|
|
679
|
+
!pointIsInsideBounds(segment.end, srj.bounds),
|
|
680
|
+
)
|
|
681
|
+
) {
|
|
682
|
+
return false
|
|
683
|
+
}
|
|
684
|
+
for (let index = 0; index < plan.segments.length; index++) {
|
|
685
|
+
if (
|
|
686
|
+
!segmentIsClearOfObstacles({
|
|
687
|
+
segment: plan.segments[index]!,
|
|
688
|
+
plan,
|
|
689
|
+
segmentIndex: index,
|
|
690
|
+
obstacles: srj.obstacles,
|
|
691
|
+
clearance,
|
|
692
|
+
})
|
|
693
|
+
) {
|
|
694
|
+
return false
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (plan.via) {
|
|
698
|
+
for (const obstacle of srj.obstacles) {
|
|
699
|
+
if (
|
|
700
|
+
!obstacle.layers.some((layer) => plan.via!.spanLayers.includes(layer))
|
|
701
|
+
) {
|
|
702
|
+
continue
|
|
703
|
+
}
|
|
704
|
+
if (
|
|
705
|
+
distancePointToObstacle(plan.via.center, obstacle) <
|
|
706
|
+
plan.via.diameter / 2 + clearance - 1e-9
|
|
707
|
+
) {
|
|
708
|
+
return false
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
for (const otherPlan of otherPlans) {
|
|
714
|
+
for (const segment of plan.segments) {
|
|
715
|
+
for (const otherSegment of otherPlan.segments) {
|
|
716
|
+
if (!segmentsAreClear(segment, otherSegment, clearance)) return false
|
|
717
|
+
}
|
|
718
|
+
if (
|
|
719
|
+
otherPlan.via?.spanLayers.includes(segment.layer) &&
|
|
720
|
+
distancePointToSegment(
|
|
721
|
+
otherPlan.via.center,
|
|
722
|
+
segment.start,
|
|
723
|
+
segment.end,
|
|
724
|
+
) <
|
|
725
|
+
otherPlan.via.diameter / 2 + segment.width / 2 + clearance - 1e-9
|
|
726
|
+
) {
|
|
727
|
+
return false
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (plan.via) {
|
|
731
|
+
for (const otherSegment of otherPlan.segments) {
|
|
732
|
+
if (
|
|
733
|
+
plan.via.spanLayers.includes(otherSegment.layer) &&
|
|
734
|
+
distancePointToSegment(
|
|
735
|
+
plan.via.center,
|
|
736
|
+
otherSegment.start,
|
|
737
|
+
otherSegment.end,
|
|
738
|
+
) <
|
|
739
|
+
plan.via.diameter / 2 + otherSegment.width / 2 + clearance - 1e-9
|
|
740
|
+
) {
|
|
741
|
+
return false
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (
|
|
745
|
+
otherPlan.via &&
|
|
746
|
+
plan.via.spanLayers.some((layer) =>
|
|
747
|
+
otherPlan.via!.spanLayers.includes(layer),
|
|
748
|
+
) &&
|
|
749
|
+
distance(plan.via.center, otherPlan.via.center) <
|
|
750
|
+
(plan.via.diameter + otherPlan.via.diameter) / 2 + clearance - 1e-9
|
|
751
|
+
) {
|
|
752
|
+
return false
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return true
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function routePlaneTerminatedBus(
|
|
760
|
+
params: RouteBusParams,
|
|
761
|
+
): FanoutRoutePlan[] | null {
|
|
762
|
+
const {
|
|
763
|
+
srj,
|
|
764
|
+
bus,
|
|
765
|
+
targetLayer,
|
|
766
|
+
acceptedPlans,
|
|
767
|
+
layerNames,
|
|
768
|
+
traceWidth,
|
|
769
|
+
viaDiameter,
|
|
770
|
+
viaHoleDiameter,
|
|
771
|
+
clearance,
|
|
772
|
+
} = params
|
|
773
|
+
const sourceObstacle = bus.connections[0]?.sourceObstacle
|
|
774
|
+
if (!sourceObstacle || bus.termination.type !== "plane") return null
|
|
775
|
+
const sourceLayer = bus.connections[0]!.sourceLayer
|
|
776
|
+
if (targetLayer === sourceLayer) return null
|
|
777
|
+
const directionalPadSize = isHorizontal(bus.direction)
|
|
778
|
+
? sourceObstacle.width
|
|
779
|
+
: sourceObstacle.height
|
|
780
|
+
const pairChannelFitsVia =
|
|
781
|
+
getDirectionalPitch(bus) / 2 - directionalPadSize / 2 >=
|
|
782
|
+
viaDiameter / 2 + clearance - 1e-9
|
|
783
|
+
const viaHandednesses: readonly ViaHandedness[] = pairChannelFitsVia
|
|
784
|
+
? [0]
|
|
785
|
+
: [1, -1]
|
|
786
|
+
|
|
787
|
+
for (const viaHandedness of viaHandednesses) {
|
|
788
|
+
for (const connectionOrder of getConnectionOrders(bus)) {
|
|
789
|
+
const candidatePlans: FanoutRoutePlan[] = []
|
|
790
|
+
let orderIsClear = true
|
|
791
|
+
for (const preparedConnection of connectionOrder) {
|
|
792
|
+
const sourceTrack = getPerpendicularAxis(
|
|
793
|
+
preparedConnection.sourcePoint,
|
|
794
|
+
bus.direction,
|
|
795
|
+
)
|
|
796
|
+
const plan = buildPlan({
|
|
797
|
+
preparedConnection,
|
|
798
|
+
bus,
|
|
799
|
+
targetLayer,
|
|
800
|
+
track: sourceTrack,
|
|
801
|
+
exitAxis: getExitAxis(bus),
|
|
802
|
+
layerNames,
|
|
803
|
+
traceWidth,
|
|
804
|
+
viaDiameter,
|
|
805
|
+
viaHoleDiameter,
|
|
806
|
+
viaHandedness,
|
|
807
|
+
interstitialEscape: !pairChannelFitsVia,
|
|
808
|
+
spreadLaneIndex: 0,
|
|
809
|
+
clearance,
|
|
810
|
+
terminateAtVia: true,
|
|
811
|
+
})
|
|
812
|
+
if (
|
|
813
|
+
!planIsClear({
|
|
814
|
+
plan,
|
|
815
|
+
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
816
|
+
srj,
|
|
817
|
+
clearance,
|
|
818
|
+
})
|
|
819
|
+
) {
|
|
820
|
+
orderIsClear = false
|
|
821
|
+
break
|
|
822
|
+
}
|
|
823
|
+
candidatePlans.push(plan)
|
|
824
|
+
}
|
|
825
|
+
if (orderIsClear) return candidatePlans
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return null
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
|
|
833
|
+
const {
|
|
834
|
+
srj,
|
|
835
|
+
bus,
|
|
836
|
+
targetLayer,
|
|
837
|
+
acceptedPlans,
|
|
838
|
+
layerNames,
|
|
839
|
+
traceWidth,
|
|
840
|
+
viaDiameter,
|
|
841
|
+
viaHoleDiameter,
|
|
842
|
+
clearance,
|
|
843
|
+
compactBusTracks,
|
|
844
|
+
} = params
|
|
845
|
+
if (bus.termination.type === "plane") {
|
|
846
|
+
return routePlaneTerminatedBus(params)
|
|
847
|
+
}
|
|
848
|
+
const exitAxis = getExitAxis(bus)
|
|
849
|
+
const sourceObstacle = bus.connections[0]?.sourceObstacle
|
|
850
|
+
if (!sourceObstacle) return []
|
|
851
|
+
const directionalPadSize = isHorizontal(bus.direction)
|
|
852
|
+
? sourceObstacle.width
|
|
853
|
+
: sourceObstacle.height
|
|
854
|
+
const sourceLayer = bus.connections[0]!.sourceLayer
|
|
855
|
+
const targetUsesVia = targetLayer !== sourceLayer
|
|
856
|
+
const outwardEdgeBus = busIsOnOutwardComponentEdge(bus)
|
|
857
|
+
const pairChannelFitsVia =
|
|
858
|
+
getDirectionalPitch(bus) / 2 - directionalPadSize / 2 >=
|
|
859
|
+
viaDiameter / 2 + clearance - 1e-9
|
|
860
|
+
const interstitialEscape =
|
|
861
|
+
targetUsesVia && !outwardEdgeBus && !pairChannelFitsVia
|
|
862
|
+
const viaHandednesses: readonly ViaHandedness[] = targetUsesVia
|
|
863
|
+
? pairChannelFitsVia || outwardEdgeBus
|
|
864
|
+
? [0]
|
|
865
|
+
: [1, -1]
|
|
866
|
+
: [0]
|
|
867
|
+
|
|
868
|
+
for (const viaHandedness of viaHandednesses) {
|
|
869
|
+
for (const connectionOrder of getConnectionOrders(bus)) {
|
|
870
|
+
const candidatePlans: FanoutRoutePlan[] = []
|
|
871
|
+
let orderIsClear = true
|
|
872
|
+
for (const preparedConnection of connectionOrder) {
|
|
873
|
+
let acceptedPlan: FanoutRoutePlan | null = null
|
|
874
|
+
for (const track of getTrackCandidates({
|
|
875
|
+
bus,
|
|
876
|
+
connection: preparedConnection,
|
|
877
|
+
preferredTrack: getPreferredTrack({
|
|
878
|
+
bus,
|
|
879
|
+
connection: preparedConnection,
|
|
880
|
+
targetUsesVia,
|
|
881
|
+
interstitialEscape,
|
|
882
|
+
compactBusTracks,
|
|
883
|
+
traceWidth,
|
|
884
|
+
viaDiameter,
|
|
885
|
+
clearance,
|
|
886
|
+
}),
|
|
887
|
+
traceWidth,
|
|
888
|
+
clearance,
|
|
889
|
+
})) {
|
|
890
|
+
const plan = buildPlan({
|
|
891
|
+
preparedConnection,
|
|
892
|
+
bus,
|
|
893
|
+
targetLayer,
|
|
894
|
+
track: track.value,
|
|
895
|
+
exitAxis,
|
|
896
|
+
layerNames,
|
|
897
|
+
traceWidth,
|
|
898
|
+
viaDiameter,
|
|
899
|
+
viaHoleDiameter,
|
|
900
|
+
viaHandedness,
|
|
901
|
+
interstitialEscape,
|
|
902
|
+
spreadLaneIndex: Math.min(
|
|
903
|
+
getConnectionRank(bus, preparedConnection),
|
|
904
|
+
bus.connections.length -
|
|
905
|
+
getConnectionRank(bus, preparedConnection) -
|
|
906
|
+
1,
|
|
907
|
+
),
|
|
908
|
+
clearance,
|
|
909
|
+
terminateAtVia: false,
|
|
910
|
+
})
|
|
911
|
+
if (
|
|
912
|
+
planIsClear({
|
|
913
|
+
plan,
|
|
914
|
+
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
915
|
+
srj,
|
|
916
|
+
clearance,
|
|
917
|
+
})
|
|
918
|
+
) {
|
|
919
|
+
acceptedPlan = plan
|
|
920
|
+
break
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (!acceptedPlan) {
|
|
924
|
+
orderIsClear = false
|
|
925
|
+
break
|
|
926
|
+
}
|
|
927
|
+
candidatePlans.push(acceptedPlan)
|
|
928
|
+
}
|
|
929
|
+
if (orderIsClear) return candidatePlans
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return null
|
|
934
|
+
}
|