@tscircuit/fanout-solver 0.0.54 → 0.0.56

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 CHANGED
@@ -306,6 +306,19 @@ To capture the inputs without solving, use `bun run generate:dataset31`.
306
306
  The optional `--dataset dataset31` flag is accepted for explicit CI invocation;
307
307
  other dataset selections are rejected.
308
308
 
309
+ To isolate a failure without changing pads or clearance rules, reduce a captured
310
+ input by bus or connection count. This is a diagnostic, not a benchmark score:
311
+
312
+ ```sh
313
+ bun scripts/debug-dataset31.ts --input benchmark-results/inputs/10-left-bottom-offset.json --buses DDR_ADDR_CTRL --connection-limit 7 --output /tmp/address-seven
314
+ bun scripts/debug-dataset31.ts --input benchmark-results/inputs/10-left-bottom-offset.json --buses DDR_ADDR_CTRL --output /tmp/address-eight
315
+ ```
316
+
317
+ Each run has a hard process deadline and saves JSON progress and an SVG preview.
318
+ Use `--dump-input <path> --capture-only` to save the exact reduced solver input
319
+ as a regression fixture. Retained buses keep their original targets and timing
320
+ limits; only removed connections and their differential-pair metadata are pruned.
321
+
309
322
  Each sample runs in an isolated process, with up to four concurrent processes
310
323
  locally and a **120-second hard timeout** by default. A synchronous solver hang,
311
324
  exception, or unsolved case does not prevent later samples from running.
@@ -1,6 +1,8 @@
1
1
  import type { SimpleRouteJson } from "@tscircuit/capacity-autorouter"
2
2
  import { BaseSolver } from "@tscircuit/solver-utils"
3
- import { shortenBusPlans } from "./shorten-bus-plans"
3
+ import { selectCompatibleCandidates } from "./select-compatible-candidates"
4
+ import { refineAdaptivePlaneReservationCore } from "./refine-adaptive-plane-reservations"
5
+ import { shouldUseAdaptiveDensePlaneRouting } from "./should-use-adaptive-dense-plane-routing"
4
6
  import { type GraphicsObject, mergeGraphics } from "graphics-debug"
5
7
  import { addViaLayerMetadataToSrj } from "./add-via-layer-metadata"
6
8
  import { getCornerBandSide, getExitEdgeForDirection } from "./boundary-exit"
@@ -34,6 +36,8 @@ import {
34
36
  } from "./route-bus"
35
37
  import { routeSingleLayerWithAdaptiveExitsSteps } from "./route-single-layer-adaptive-exits"
36
38
  import { routeSingleLayerWithPushAndShove } from "./route-single-layer-push-shove"
39
+ import { getRuntimeProcess } from "./runtime-process"
40
+ import { shortenBusPlans } from "./shorten-bus-plans"
37
41
  import type {
38
42
  AssignmentAttempt,
39
43
  Bounds,
@@ -49,6 +53,11 @@ import type {
49
53
  import { validateFanoutSolution } from "./validate-fanout-solution"
50
54
  import { visualizeSimpleRouteJson } from "./visualize-simple-route-json"
51
55
 
56
+ // Browser Web Workers do not expose Node's `process` global. Keep a local,
57
+ // browser-safe adapter so the optional debug environment flags still work in
58
+ // Node and Bun without crashing browser-based routing.
59
+ const process = getRuntimeProcess(globalThis)
60
+
52
61
  interface ResolvedFanoutConfig {
53
62
  traceWidth: number
54
63
  viaDiameter: number
@@ -1035,7 +1044,11 @@ export class FanoutSolver extends BaseSolver {
1035
1044
  escapeLayersByBusId: this.escapeLayersByBusId,
1036
1045
  preferOrderedCoordinatedWindingLayers:
1037
1046
  this.config.densePlaneReservationBusIds.length > 0 ||
1038
- this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0,
1047
+ this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0 ||
1048
+ shouldUseAdaptiveDensePlaneRouting(
1049
+ this.preparedBuses,
1050
+ this.config.allowBlindAndBuriedVias,
1051
+ ),
1039
1052
  }),
1040
1053
  generatedAssignments,
1041
1054
  maxAssignments: this.config.maxLayerCombinations,
@@ -1383,6 +1396,8 @@ export class FanoutSolver extends BaseSolver {
1383
1396
  busesInRoutingOrder: readonly PreparedBus[]
1384
1397
  denseRoutingStrategy?: "pad-aligned" | "boundary-aligned"
1385
1398
  lengthMatchingStage?: "before-planes" | "after-planes"
1399
+ promotedPlaneReservationBusIds?: readonly string[]
1400
+ planeReservationRetryCount?: number
1386
1401
  }): Generator<FanoutWorkYield, MixedTerminationState | null, unknown> {
1387
1402
  if (this.config.allowBlindAndBuriedVias) return null
1388
1403
  const usePadAlignedDenseRouting =
@@ -1398,6 +1413,8 @@ export class FanoutSolver extends BaseSolver {
1398
1413
  "plane-match:incremental-failed",
1399
1414
  "plane-route:alternate-candidate-counts",
1400
1415
  "plane-route:alternate-search",
1416
+ "plane-route:csp",
1417
+ "plane-reservation-core",
1401
1418
  "plane-route:promote-failed",
1402
1419
  "plane-route:alternate-choice",
1403
1420
  "plane-route:promote-zero-candidates",
@@ -1416,9 +1433,20 @@ export class FanoutSolver extends BaseSolver {
1416
1433
  const unsortedBoundaryBuses = params.busesInRoutingOrder.filter(
1417
1434
  (bus) => bus.termination.type === "boundary",
1418
1435
  )
1419
- const useConfiguredDensePlaneRouting =
1436
+ const configuredDensePlaneRouting =
1420
1437
  this.config.densePlaneReservationBusIds.length > 0 ||
1421
1438
  this.config.denseUnrestrictedPlaneRoutingBusIds.length > 0
1439
+ const useAdaptiveDensePlaneRouting =
1440
+ !configuredDensePlaneRouting &&
1441
+ shouldUseAdaptiveDensePlaneRouting(
1442
+ this.preparedBuses,
1443
+ this.config.allowBlindAndBuriedVias,
1444
+ )
1445
+ const useConfiguredDensePlaneRouting =
1446
+ configuredDensePlaneRouting || useAdaptiveDensePlaneRouting
1447
+ const useAdaptiveJointPlaneSelection =
1448
+ useAdaptiveDensePlaneRouting &&
1449
+ (params.planeReservationRetryCount ?? 0) === 0
1422
1450
  const matchLengthsAfterPlanes =
1423
1451
  useConfiguredDensePlaneRouting &&
1424
1452
  params.lengthMatchingStage !== "before-planes"
@@ -1657,6 +1685,17 @@ export class FanoutSolver extends BaseSolver {
1657
1685
  : 8,
1658
1686
  )
1659
1687
  : planeBuses
1688
+ if (params.promotedPlaneReservationBusIds) {
1689
+ activeBoundaryReservationPlaneBuses = [
1690
+ ...new Set([
1691
+ ...activeBoundaryReservationPlaneBuses,
1692
+ ...planeBuses.filter((bus) =>
1693
+ params.promotedPlaneReservationBusIds!.includes(bus.busId),
1694
+ ),
1695
+ ]),
1696
+ ]
1697
+ }
1698
+ const unroutablePlaneBusIds = new Set<string>()
1660
1699
  debugDense(
1661
1700
  "start",
1662
1701
  boundaryBuses.map((bus) => `${bus.busId}:${bus.connections.length}`),
@@ -1713,7 +1752,7 @@ export class FanoutSolver extends BaseSolver {
1713
1752
  getCornerBandSide(bus.exitEdge, bus.preferredExit)
1714
1753
  ? getDenseSingletonBoundaryGeometry(bus).targetProjection > 0
1715
1754
  : getExitEdgeForDirection(bus.direction) !== bus.exitEdge
1716
- : bus.exitEdge === "top" && bus.connections.length >= 8
1755
+ : hasThreeWideBoundaryBuses && bus.connections.length >= 8
1717
1756
  ? false
1718
1757
  : getExitEdgeForDirection(bus.direction) !== bus.exitEdge,
1719
1758
  ]),
@@ -2108,6 +2147,16 @@ export class FanoutSolver extends BaseSolver {
2108
2147
  return [
2109
2148
  {
2110
2149
  connectionName: connection.connection.name,
2150
+ sourceEscapeSegment: matchedPlans.some(
2151
+ (plan) => plan.connectionIndex === connection.connectionIndex,
2152
+ )
2153
+ ? undefined
2154
+ : {
2155
+ start: connection.sourcePoint,
2156
+ end: center,
2157
+ layer: connection.sourceLayer,
2158
+ width: this.config.traceWidth,
2159
+ },
2111
2160
  via: {
2112
2161
  center,
2113
2162
  diameter: this.config.viaDiameter,
@@ -2169,7 +2218,7 @@ export class FanoutSolver extends BaseSolver {
2169
2218
  fixedViaPointsByConnectionIndex,
2170
2219
  reservedVias: getReservedVias(bus),
2171
2220
  viaMinimalOnly: process.env.FANOUT_DEBUG_ALLOW_EXTRA_VIAS !== "1",
2172
- allowBoundarySideViaFallback: true,
2221
+ allowBoundarySideViaFallback: bus.connections.length === 1,
2173
2222
  preferCornerBoundaryVia: useConfiguredDensePlaneRouting,
2174
2223
  adaptiveWindingRouteOrder,
2175
2224
  alignWindingGridToPads:
@@ -2310,7 +2359,21 @@ export class FanoutSolver extends BaseSolver {
2310
2359
  ))[0]
2311
2360
  if (busPlans) break
2312
2361
  }
2313
- if (!busPlans) fixedViaPointsByConnectionIndex = originalPoints
2362
+ if (!busPlans) {
2363
+ fixedViaPointsByConnectionIndex = originalPoints
2364
+ if (bus.connections.length === 2) {
2365
+ busPlans = (yield* routeAlternatives(
2366
+ {
2367
+ ...routeParams,
2368
+ fixedViaPointsByConnectionIndex: originalPoints,
2369
+ reservedVias: getReservedVias(bus),
2370
+ allowBoundarySideViaFallback: true,
2371
+ fixedViaFallbackRouteOrderAttempts: 1,
2372
+ },
2373
+ 1,
2374
+ ))[0]
2375
+ }
2376
+ }
2314
2377
  }
2315
2378
  if (busPlans && bus.maxLengthSkew !== undefined) {
2316
2379
  const lengths = busPlans.map((plan) => plan.length)
@@ -3068,6 +3131,79 @@ export class FanoutSolver extends BaseSolver {
3068
3131
  })),
3069
3132
  }),
3070
3133
  )
3134
+ if (useAdaptiveJointPlaneSelection) {
3135
+ const acceptedPlans = [
3136
+ ...candidatePlans,
3137
+ ...feasibleAlternatePlanePlans,
3138
+ ]
3139
+ for (const planeBus of planeBuses) {
3140
+ if (
3141
+ matchedPlaneBuses.includes(planeBus) ||
3142
+ candidateSets.some((set) => set.planeBus === planeBus)
3143
+ )
3144
+ continue
3145
+ const candidates: IndependentPlaneRouteCandidate[] = []
3146
+ const sites = getComponentDogboneViaSiteCandidates(
3147
+ [planeBus],
3148
+ {
3149
+ viaDiameter: this.config.viaDiameter,
3150
+ viaHoleDiameter: this.config.viaHoleDiameter,
3151
+ traceWidth: this.config.traceWidth,
3152
+ clearance: this.config.clearance,
3153
+ blockingSegments: acceptedPlans.flatMap((plan) =>
3154
+ plan.segments.map((segment) => ({
3155
+ connectionIndex: plan.connectionIndex,
3156
+ segment,
3157
+ })),
3158
+ ),
3159
+ blockingVias: acceptedPlans.flatMap((plan) =>
3160
+ [
3161
+ plan.via,
3162
+ ...(plan.additionalVias ?? []),
3163
+ plan.planeEndpointVia,
3164
+ ]
3165
+ .filter((via) => via !== undefined)
3166
+ .map((via) => ({
3167
+ connectionIndex: plan.connectionIndex,
3168
+ center: via!.center,
3169
+ diameter: via!.diameter,
3170
+ spanLayers: via!.spanLayers,
3171
+ })),
3172
+ ),
3173
+ additionalObstacles: this.routingSrj.obstacles,
3174
+ preferPlaneCheckerboardSites: true,
3175
+ canShareCopper,
3176
+ },
3177
+ )
3178
+ for (const site of sites) {
3179
+ const plans = routeBus({
3180
+ srj: this.routingSrj,
3181
+ bus: planeBus,
3182
+ targetLayer:
3183
+ params.busLayerAssignments[planeBus.busId]!,
3184
+ acceptedPlans,
3185
+ layerNames: this.config.layerNames,
3186
+ traceWidth: this.config.traceWidth,
3187
+ viaDiameter: this.config.viaDiameter,
3188
+ viaHoleDiameter: this.config.viaHoleDiameter,
3189
+ clearance: this.config.clearance,
3190
+ compactBusTracks: this.config.compactBusTracks,
3191
+ allowBlindAndBuriedVias: false,
3192
+ allowSameNetMerges: this.config.allowSameNetMerges,
3193
+ fixedViaPointsByConnectionIndex: new Map([
3194
+ [site.connectionIndex, site.point],
3195
+ ]),
3196
+ })
3197
+ if (plans)
3198
+ candidates.push({
3199
+ key: `${planeBus.busId}:local:${candidates.length}`,
3200
+ planeBus,
3201
+ plans,
3202
+ })
3203
+ }
3204
+ candidateSets.push({ planeBus, candidates })
3205
+ }
3206
+ }
3071
3207
  debugDense(
3072
3208
  "plane-route:alternate-candidate-counts",
3073
3209
  candidateSets.map((candidateSet) => [
@@ -3075,6 +3211,10 @@ export class FanoutSolver extends BaseSolver {
3075
3211
  candidateSet.candidates.length,
3076
3212
  ]),
3077
3213
  )
3214
+ for (const candidateSet of candidateSets) {
3215
+ if (candidateSet.candidates.length === 0)
3216
+ unroutablePlaneBusIds.add(candidateSet.planeBus.busId)
3217
+ }
3078
3218
  const compatibilityByCandidatePair = new Map<string, boolean>()
3079
3219
  const candidatesAreCompatible = (
3080
3220
  first: IndependentPlaneRouteCandidate,
@@ -3201,10 +3341,82 @@ export class FanoutSolver extends BaseSolver {
3201
3341
  )
3202
3342
  return null
3203
3343
  }
3204
- const selectedCandidates = selectCompatiblePlaneRoutes(
3205
- candidateSets,
3206
- [],
3207
- )
3344
+ let selectedCandidates: IndependentPlaneRouteCandidate[] | null
3345
+ if (useAdaptiveDensePlaneRouting) {
3346
+ const getUniqueDomains = (sets: typeof candidateSets) =>
3347
+ sets.map((set) => [
3348
+ ...new Map(
3349
+ set.candidates.map((candidate) => [
3350
+ JSON.stringify(
3351
+ candidate.plans.map((plan) => [
3352
+ plan.segments,
3353
+ plan.via,
3354
+ plan.additionalVias,
3355
+ plan.planeEndpointSegments,
3356
+ plan.planeEndpointVia,
3357
+ ]),
3358
+ ),
3359
+ candidate,
3360
+ ]),
3361
+ ).values(),
3362
+ ])
3363
+ const runCandidateSelection = (
3364
+ sets: typeof candidateSets,
3365
+ maximumSearchStates = maximumAlternatePlaneSearchStates,
3366
+ ) =>
3367
+ selectCompatibleCandidates({
3368
+ candidateSets: getUniqueDomains(sets),
3369
+ maximumSearchStates,
3370
+ areCompatible: (first, second) =>
3371
+ fanoutPlansAreMutuallyClear({
3372
+ plans: [...first.plans, ...second.plans],
3373
+ srj: this.routingSrj,
3374
+ clearance: this.config.clearance,
3375
+ allowSameNetMerges: this.config.allowSameNetMerges,
3376
+ }),
3377
+ })
3378
+ const result = runCandidateSelection(candidateSets)
3379
+ debugDense(
3380
+ "plane-route:csp",
3381
+ result.selection ? "complete" : "failed",
3382
+ result.searchStates,
3383
+ result.emptyDomainIndices.map(
3384
+ (index) => candidateSets[index]!.planeBus.busId,
3385
+ ),
3386
+ )
3387
+ selectedCandidates = result.selection
3388
+ alternatePlaneSearchStates = result.searchStates
3389
+ if (!result.selection && useAdaptiveDensePlaneRouting) {
3390
+ const expanded = result.emptyDomainIndices
3391
+ .map((index) => candidateSets[index]!.planeBus.busId)
3392
+ .filter(
3393
+ (id) =>
3394
+ !orderedAlternatePlaneBuses.some(
3395
+ (bus) => bus.busId === id,
3396
+ ),
3397
+ )
3398
+ if (expanded.length > 0) {
3399
+ debugDense("plane-route:expand-domains", expanded)
3400
+ return matchViaPointsAroundPlans(
3401
+ candidatePlans,
3402
+ new Set([...promotedAlternatePlaneBusIds, ...expanded]),
3403
+ )
3404
+ }
3405
+ }
3406
+ if (!result.selection)
3407
+ for (const index of (params.planeReservationRetryCount ??
3408
+ 0) === 0
3409
+ ? result.conflictDomainIndices
3410
+ : result.emptyDomainIndices.slice(0, 1))
3411
+ unroutablePlaneBusIds.add(
3412
+ candidateSets[index]!.planeBus.busId,
3413
+ )
3414
+ } else {
3415
+ selectedCandidates = selectCompatiblePlaneRoutes(
3416
+ candidateSets,
3417
+ [],
3418
+ )
3419
+ }
3208
3420
  alternatePlanePlans = selectedCandidates
3209
3421
  ? [
3210
3422
  ...feasibleAlternatePlanePlans,
@@ -3230,6 +3442,18 @@ export class FanoutSolver extends BaseSolver {
3230
3442
  )
3231
3443
  if (!alternatePlanePlans) return null
3232
3444
  feasibleAlternatePlanePlans = alternatePlanePlans
3445
+ if (useAdaptiveJointPlaneSelection) {
3446
+ matchedPlaneBusesInRoutingOrder = []
3447
+ return new Map([
3448
+ ...fixedBoundaryViaPoints,
3449
+ ...alternatePlanePlans
3450
+ .filter((plan) => plan.via)
3451
+ .map(
3452
+ (plan) =>
3453
+ [plan.connectionIndex, plan.via!.center] as const,
3454
+ ),
3455
+ ])
3456
+ }
3233
3457
  }
3234
3458
  let planeBusIdsRoutedWithoutDogbones = new Set<string>()
3235
3459
  let allBlockingSegments = [...blockingSegments]
@@ -3679,6 +3903,39 @@ export class FanoutSolver extends BaseSolver {
3679
3903
  }
3680
3904
  }
3681
3905
 
3906
+ if (
3907
+ useAdaptiveDensePlaneRouting &&
3908
+ unroutablePlaneBusIds.size > 0 &&
3909
+ (params.planeReservationRetryCount ?? 0) < 5
3910
+ ) {
3911
+ const newlyPromoted = [...unroutablePlaneBusIds].filter(
3912
+ (id) =>
3913
+ !activeBoundaryReservationPlaneBuses.some((bus) => bus.busId === id),
3914
+ )
3915
+ const refinedPromotions =
3916
+ (params.planeReservationRetryCount ?? 0) === 0
3917
+ ? refineAdaptivePlaneReservationCore({
3918
+ candidateBusIds: newlyPromoted,
3919
+ activeBusIds: new Set(
3920
+ activeBoundaryReservationPlaneBuses.map((bus) => bus.busId),
3921
+ ),
3922
+ planeBuses,
3923
+ })
3924
+ : newlyPromoted
3925
+ if (refinedPromotions.length > 0) {
3926
+ debugDense("plane-reservation-core", refinedPromotions)
3927
+ debugDense("promote-reservations", refinedPromotions)
3928
+ return yield* this.routeDenseThroughAllMixedTerminationSteps({
3929
+ ...params,
3930
+ promotedPlaneReservationBusIds: [
3931
+ ...(params.promotedPlaneReservationBusIds ?? []),
3932
+ ...refinedPromotions,
3933
+ ],
3934
+ planeReservationRetryCount:
3935
+ (params.planeReservationRetryCount ?? 0) + 1,
3936
+ })
3937
+ }
3938
+ }
3682
3939
  if (matchLengthsAfterPlanes) {
3683
3940
  return yield* this.routeDenseThroughAllMixedTerminationSteps({
3684
3941
  ...params,
@@ -0,0 +1,26 @@
1
+ import type { FanoutDirection, Point2D } from "./types"
2
+
3
+ /** A bounded fallback for channels trapped by uniform outward dogbones. */
4
+ export function getDogboneSideVariants(
5
+ connections: readonly { connectionIndex: number; sourcePoint: Point2D }[],
6
+ direction: FanoutDirection,
7
+ ): number[][] {
8
+ const axis = direction === "up" || direction === "down" ? "y" : "x"
9
+ const variants = connections
10
+ .slice(0, 32)
11
+ .map(({ connectionIndex }) => [connectionIndex])
12
+ // Two pads in the same escape row can jointly close a winding corridor.
13
+ // Move both to the other half-pitch row, keeping every other site unchanged.
14
+ // Do not enumerate the exponential Cartesian product of all via sites.
15
+ for (let first = 0; first < connections.length; first++) {
16
+ for (let second = first + 1; second < connections.length; second++) {
17
+ if (variants.length >= 32) return variants
18
+ const a = connections[first]!
19
+ const b = connections[second]!
20
+ if (Math.abs(a.sourcePoint[axis] - b.sourcePoint[axis]) <= 1e-9) {
21
+ variants.push([a.connectionIndex, b.connectionIndex])
22
+ }
23
+ }
24
+ }
25
+ return variants
26
+ }
@@ -0,0 +1,94 @@
1
+ import type { PreparedBus } from "./types"
2
+
3
+ const EPSILON = 1e-9
4
+
5
+ /**
6
+ * A failed arc-consistency chain ends with the domain that removed the last
7
+ * supported candidate. When that plane pad has an adjacent peer farther into
8
+ * the component, reserving the peer keeps the outer pad available for a
9
+ * displaced route while protecting the same local dogbone corridor.
10
+ */
11
+ export function refineAdaptivePlaneReservationCore(params: {
12
+ candidateBusIds: readonly string[]
13
+ activeBusIds: ReadonlySet<string>
14
+ planeBuses: readonly PreparedBus[]
15
+ }): string[] {
16
+ const core = params.candidateBusIds.filter(
17
+ (busId) => !params.activeBusIds.has(busId),
18
+ )
19
+ if (core.length < 2) return core
20
+
21
+ const terminalIndex = core.length - 1
22
+ const terminal = params.planeBuses.find(
23
+ (bus) => bus.busId === core[terminalIndex],
24
+ )
25
+ const source = terminal?.connections[0]?.sourcePoint
26
+ if (
27
+ !terminal ||
28
+ !source ||
29
+ terminal.connections.length !== 1 ||
30
+ terminal.termination.type !== "plane"
31
+ )
32
+ return core
33
+ const terminalLayer = terminal.termination.layer
34
+
35
+ const perpendicularAxis =
36
+ terminal.direction === "up" || terminal.direction === "down" ? "x" : "y"
37
+ const parallelAxis = perpendicularAxis === "x" ? "y" : "x"
38
+ const componentCoordinates =
39
+ perpendicularAxis === "x" ? terminal.xCoordinates : terminal.yCoordinates
40
+ const pitch = perpendicularAxis === "x" ? terminal.pitchX : terminal.pitchY
41
+ if (
42
+ componentCoordinates.length === 0 ||
43
+ !Number.isFinite(pitch) ||
44
+ pitch <= EPSILON
45
+ ) {
46
+ return core
47
+ }
48
+
49
+ const componentCenter =
50
+ (Math.min(...componentCoordinates) + Math.max(...componentCoordinates)) / 2
51
+ const inwardSign = Math.sign(componentCenter - source[perpendicularAxis])
52
+ if (inwardSign === 0) return core
53
+
54
+ const coreIds = new Set(core)
55
+ const replacement = params.planeBuses
56
+ .filter((bus) => {
57
+ const candidateSource = bus.connections[0]?.sourcePoint
58
+ if (
59
+ bus === terminal ||
60
+ bus.componentId !== terminal.componentId ||
61
+ bus.termination.type !== "plane" ||
62
+ bus.termination.layer !== terminalLayer ||
63
+ bus.direction !== terminal.direction ||
64
+ bus.connections.length !== 1 ||
65
+ !candidateSource ||
66
+ params.activeBusIds.has(bus.busId) ||
67
+ coreIds.has(bus.busId) ||
68
+ Math.abs(candidateSource[parallelAxis] - source[parallelAxis]) > EPSILON
69
+ ) {
70
+ return false
71
+ }
72
+ const inwardDistance =
73
+ (candidateSource[perpendicularAxis] - source[perpendicularAxis]) *
74
+ inwardSign
75
+ return inwardDistance > EPSILON && inwardDistance <= pitch + EPSILON
76
+ })
77
+ .toSorted((first, second) => {
78
+ const firstSource = first.connections[0]!.sourcePoint
79
+ const secondSource = second.connections[0]!.sourcePoint
80
+ return (
81
+ Math.abs(firstSource[perpendicularAxis] - source[perpendicularAxis]) -
82
+ Math.abs(
83
+ secondSource[perpendicularAxis] - source[perpendicularAxis],
84
+ ) ||
85
+ first.connections[0]!.connectionIndex -
86
+ second.connections[0]!.connectionIndex
87
+ )
88
+ })[0]
89
+
90
+ if (!replacement) return core
91
+ return core.map((busId, index) =>
92
+ index === terminalIndex ? replacement.busId : busId,
93
+ )
94
+ }