@tscircuit/schematic-trace-solver 0.0.163 → 0.0.165

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/dist/index.d.ts CHANGED
@@ -1394,11 +1394,15 @@ declare class NetLabelToTraceSolver extends BaseSolver {
1394
1394
  activeSubSolver: SchematicTraceSingleLineSolver2 | null;
1395
1395
  constructor(input: InlineNetLabelOutput);
1396
1396
  getConstructorParams(): [InlineNetLabelOutput];
1397
- private isEligiblePortOnlyDirectConnectionLabel;
1397
+ private isPortOnlyFallbackLabel;
1398
+ private isDirectConnectionLabel;
1399
+ private getMultiPinNetConnection;
1398
1400
  private buildCandidatePairs;
1401
+ private buildRoutedComponentCandidates;
1399
1402
  private isSupersededConnectorTrace;
1400
1403
  private routeIntersectsRemainingLabels;
1401
1404
  private tryAcceptCurrentRoute;
1405
+ private areCandidatePinsAlreadyConnected;
1402
1406
  _step(): void;
1403
1407
  getOutput(): {
1404
1408
  traces: SolvedTracePath[];
package/dist/index.js CHANGED
@@ -1507,13 +1507,17 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
1507
1507
  excludeRectsForSegment: (segIndex2) => {
1508
1508
  const lastSegIndex2 = path.length - 2;
1509
1509
  const excludedRects = /* @__PURE__ */ new Set();
1510
- if (segIndex2 === 0 || segIndex2 === lastSegIndex2) {
1510
+ const segmentStart = path[segIndex2];
1511
+ const segmentEnd = path[segIndex2 + 1];
1512
+ const oppositeTerminalPin = segIndex2 === 0 ? PB : segIndex2 === lastSegIndex2 ? PA : null;
1513
+ if (oppositeTerminalPin) {
1511
1514
  for (const textObstacle of this.textObstacles) {
1512
- excludedRects.add(textObstacle);
1515
+ const textBounds = getTextBoxBounds(textObstacle.textBox);
1516
+ if (textObstacle.textBox.chipId !== oppositeTerminalPin.chipId || !segmentIntersectsRect(segmentStart, segmentEnd, textBounds)) {
1517
+ excludedRects.add(textObstacle);
1518
+ }
1513
1519
  }
1514
1520
  }
1515
- const segmentStart = path[segIndex2];
1516
- const segmentEnd = path[segIndex2 + 1];
1517
1521
  const endpointEpsilon = 1e-9;
1518
1522
  const segmentTouchesPin = (pin) => [segmentStart, segmentEnd].some(
1519
1523
  (point) => Math.abs(point.x - pin.x) <= endpointEpsilon && Math.abs(point.y - pin.y) <= endpointEpsilon
@@ -15047,6 +15051,8 @@ var getTraceRecoveryConnectivityMaps = (inputProblem) => {
15047
15051
  // lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts
15048
15052
  var AVAILABLE_NET_ORIENTATION_PREFIX = "available-net-orientation-";
15049
15053
  var RECOVERED_TRACE_PREFIX = "net-label-to-trace-";
15054
+ var MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET = 0.05;
15055
+ var MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET = 0.25;
15050
15056
  var getCanonicalPairKey = (firstPinId, secondPinId) => [firstPinId, secondPinId].sort().join("--");
15051
15057
  var pathIntersectsRenderedLabel = (path, label) => {
15052
15058
  let width = label.width;
@@ -15067,12 +15073,21 @@ var pathIntersectsRenderedLabel = (path, label) => {
15067
15073
  }
15068
15074
  return false;
15069
15075
  };
15070
- var getPerpendicularOffset = (firstPin, secondPin) => {
15071
- const xDistance = Math.abs(firstPin.x - secondPin.x);
15072
- const yDistance = Math.abs(firstPin.y - secondPin.y);
15076
+ var getPerpendicularOffset = (firstPoint, secondPoint) => {
15077
+ const xDistance = Math.abs(firstPoint.x - secondPoint.x);
15078
+ const yDistance = Math.abs(firstPoint.y - secondPoint.y);
15073
15079
  if (xDistance >= yDistance) return yDistance;
15074
15080
  return xDistance;
15075
15081
  };
15082
+ var arePinsCoFacingAlongSeparationAxis = (firstPin, secondPin) => {
15083
+ if (firstPin._facingDirection !== secondPin._facingDirection) return false;
15084
+ const xDistance = Math.abs(firstPin.x - secondPin.x);
15085
+ const yDistance = Math.abs(firstPin.y - secondPin.y);
15086
+ if (xDistance >= yDistance) {
15087
+ return firstPin._facingDirection === "x+" || firstPin._facingDirection === "x-";
15088
+ }
15089
+ return firstPin._facingDirection === "y+" || firstPin._facingDirection === "y-";
15090
+ };
15076
15091
  var NetLabelToTraceSolver = class extends BaseSolver {
15077
15092
  constructor(input) {
15078
15093
  super();
@@ -15100,26 +15115,36 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15100
15115
  getConstructorParams() {
15101
15116
  return [this.input];
15102
15117
  }
15103
- isEligiblePortOnlyDirectConnectionLabel(label, groundGlobalConnNetId) {
15104
- if (label.pinIds.length !== 1 || label.mspConnectionPairIds.length !== 0 || !label.netId || label.netId === "GND" || label.globalConnNetId === groundGlobalConnNetId) {
15105
- return false;
15106
- }
15118
+ isPortOnlyFallbackLabel(label, groundGlobalConnNetIds) {
15119
+ return !(label.pinIds.length !== 1 || label.mspConnectionPairIds.length !== 0 || !label.netId || groundGlobalConnNetIds.has(label.globalConnNetId));
15120
+ }
15121
+ isDirectConnectionLabel(label) {
15122
+ if (!label.netId || label.pinIds.length !== 1) return false;
15107
15123
  const pinId = label.pinIds[0];
15108
15124
  return this.inputProblem.directConnections.some(
15109
15125
  (connection) => connection.netId === label.netId && connection.pinIds.includes(pinId)
15110
15126
  );
15111
15127
  }
15128
+ getMultiPinNetConnection(label) {
15129
+ if (!label.netId || label.pinIds.length !== 1) return void 0;
15130
+ const pinId = label.pinIds[0];
15131
+ return this.inputProblem.netConnections.find(
15132
+ (connection) => connection.isGround === false && connection.pinIds.length > 2 && connection.netId === label.netId && connection.pinIds.includes(pinId)
15133
+ );
15134
+ }
15112
15135
  buildCandidatePairs() {
15113
15136
  const { netConnMap } = getConnectivityMapsFromInputProblem(
15114
15137
  this.inputProblem
15115
15138
  );
15116
- const groundGlobalConnNetId = netConnMap.getNetConnectedToId("GND") ?? void 0;
15139
+ const groundGlobalConnNetIds = /* @__PURE__ */ new Set();
15140
+ for (const connection of this.inputProblem.netConnections) {
15141
+ if (!connection.isGround) continue;
15142
+ const globalConnNetId = netConnMap.getNetConnectedToId(connection.netId);
15143
+ if (globalConnNetId) groundGlobalConnNetIds.add(globalConnNetId);
15144
+ }
15117
15145
  const labelsByGlobalNet = /* @__PURE__ */ new Map();
15118
15146
  for (const label of this.input.netLabelPlacements) {
15119
- if (!this.isEligiblePortOnlyDirectConnectionLabel(
15120
- label,
15121
- groundGlobalConnNetId
15122
- )) {
15147
+ if (!this.isPortOnlyFallbackLabel(label, groundGlobalConnNetIds) || !this.isDirectConnectionLabel(label) && !this.getMultiPinNetConnection(label)) {
15123
15148
  continue;
15124
15149
  }
15125
15150
  const labels = labelsByGlobalNet.get(label.globalConnNetId) ?? [];
@@ -15135,6 +15160,16 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15135
15160
  const firstPin = this.pinMap.get(firstLabel.pinIds[0]);
15136
15161
  const secondPin = this.pinMap.get(secondLabel.pinIds[0]);
15137
15162
  if (!firstPin || !secondPin) continue;
15163
+ const perpendicularOffset = getPerpendicularOffset(
15164
+ firstPin,
15165
+ secondPin
15166
+ );
15167
+ const bothLabelsBelongToDirectConnections = this.isDirectConnectionLabel(firstLabel) && this.isDirectConnectionLabel(secondLabel);
15168
+ const firstMultiPinNetConnection = this.getMultiPinNetConnection(firstLabel);
15169
+ const secondMultiPinNetConnection = this.getMultiPinNetConnection(secondLabel);
15170
+ if (!bothLabelsBelongToDirectConnections && (!firstMultiPinNetConnection || firstMultiPinNetConnection !== secondMultiPinNetConnection || perpendicularOffset > MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET)) {
15171
+ continue;
15172
+ }
15138
15173
  if (arePinsInDifferentSchematicSections(
15139
15174
  this.inputProblem,
15140
15175
  firstPin,
@@ -15155,18 +15190,103 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15155
15190
  firstLabel,
15156
15191
  secondLabel,
15157
15192
  pins: [firstPin, secondPin],
15158
- perpendicularOffset: getPerpendicularOffset(firstPin, secondPin),
15193
+ perpendicularOffset,
15159
15194
  routeDistance: Math.abs(firstPin.x - secondPin.x) + Math.abs(firstPin.y - secondPin.y),
15160
- key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId)
15195
+ key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
15196
+ recoveryMode: "fallback_labels"
15161
15197
  });
15162
15198
  }
15163
15199
  }
15164
15200
  }
15201
+ candidates.push(...this.buildRoutedComponentCandidates());
15165
15202
  candidates.sort(
15166
15203
  (first, second) => first.perpendicularOffset - second.perpendicularOffset || first.routeDistance - second.routeDistance || first.key.localeCompare(second.key)
15167
15204
  );
15168
15205
  return candidates;
15169
15206
  }
15207
+ buildRoutedComponentCandidates() {
15208
+ const { netConnMap } = getConnectivityMapsFromInputProblem(
15209
+ this.inputProblem
15210
+ );
15211
+ const candidates = [];
15212
+ for (const connection of this.inputProblem.netConnections) {
15213
+ if (connection.pinIds.length <= 2 || connection.isGround !== false)
15214
+ continue;
15215
+ const globalConnNetId = netConnMap.getNetConnectedToId(connection.netId);
15216
+ if (!globalConnNetId) continue;
15217
+ const traceConnectedPinComponents = getTraceConnectedPinComponents({
15218
+ pinIds: connection.pinIds,
15219
+ traces: this.outputTraces.filter(
15220
+ (trace) => trace.globalConnNetId === globalConnNetId
15221
+ )
15222
+ }).filter((component) => component.traces.length > 0);
15223
+ for (let firstIndex = 0; firstIndex < traceConnectedPinComponents.length; firstIndex++) {
15224
+ for (let secondIndex = firstIndex + 1; secondIndex < traceConnectedPinComponents.length; secondIndex++) {
15225
+ const firstComponent = traceConnectedPinComponents[firstIndex];
15226
+ const secondComponent = traceConnectedPinComponents[secondIndex];
15227
+ const firstLabel = this.input.netLabelPlacements.find(
15228
+ (label) => label.globalConnNetId === globalConnNetId && label.mspConnectionPairIds.length > 0 && label.pinIds.some(
15229
+ (pinId) => firstComponent.pinIds.includes(pinId)
15230
+ )
15231
+ );
15232
+ const secondLabel = this.input.netLabelPlacements.find(
15233
+ (label) => label.globalConnNetId === globalConnNetId && label.mspConnectionPairIds.length > 0 && label.pinIds.some(
15234
+ (pinId) => secondComponent.pinIds.includes(pinId)
15235
+ )
15236
+ );
15237
+ if (!firstLabel || !secondLabel || firstLabel === secondLabel)
15238
+ continue;
15239
+ if (getPerpendicularOffset(
15240
+ firstLabel.anchorPoint,
15241
+ secondLabel.anchorPoint
15242
+ ) > MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET) {
15243
+ continue;
15244
+ }
15245
+ let bestCandidate;
15246
+ for (const firstPinId of firstComponent.pinIds) {
15247
+ for (const secondPinId of secondComponent.pinIds) {
15248
+ const firstPin = this.pinMap.get(firstPinId);
15249
+ const secondPin = this.pinMap.get(secondPinId);
15250
+ if (!firstPin || !secondPin) continue;
15251
+ const perpendicularOffset = getPerpendicularOffset(
15252
+ firstPin,
15253
+ secondPin
15254
+ );
15255
+ if (perpendicularOffset > MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET || arePinsCoFacingAlongSeparationAxis(firstPin, secondPin) || arePinsInDifferentSchematicSections(
15256
+ this.inputProblem,
15257
+ firstPin,
15258
+ secondPin
15259
+ ) || doesPairCrossRestrictedCenterLines({
15260
+ inputProblem: this.inputProblem,
15261
+ chipMap: this.chipMap,
15262
+ pinIdMap: this.pinMap,
15263
+ p1: firstPin,
15264
+ p2: secondPin
15265
+ })) {
15266
+ continue;
15267
+ }
15268
+ const routeDistance = Math.abs(firstPin.x - secondPin.x) + Math.abs(firstPin.y - secondPin.y);
15269
+ const candidate = {
15270
+ firstLabel,
15271
+ secondLabel,
15272
+ pins: [firstPin, secondPin],
15273
+ perpendicularOffset,
15274
+ routeDistance,
15275
+ key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
15276
+ recoveryMode: "routed_components",
15277
+ netConnectionPinIds: connection.pinIds
15278
+ };
15279
+ if (!bestCandidate || candidate.routeDistance < bestCandidate.routeDistance || candidate.routeDistance === bestCandidate.routeDistance && (candidate.perpendicularOffset < bestCandidate.perpendicularOffset || candidate.perpendicularOffset === bestCandidate.perpendicularOffset && candidate.key.localeCompare(bestCandidate.key) < 0)) {
15280
+ bestCandidate = candidate;
15281
+ }
15282
+ }
15283
+ }
15284
+ if (bestCandidate) candidates.push(bestCandidate);
15285
+ }
15286
+ }
15287
+ }
15288
+ return candidates;
15289
+ }
15170
15290
  isSupersededConnectorTrace(trace, candidate) {
15171
15291
  if (!trace.mspPairId.startsWith(AVAILABLE_NET_ORIENTATION_PREFIX)) {
15172
15292
  return false;
@@ -15192,7 +15312,13 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15192
15312
  const retainedTraces = this.outputTraces.filter(
15193
15313
  (trace) => !this.isSupersededConnectorTrace(trace, candidate)
15194
15314
  );
15195
- if (doesTraceOverlapWithExistingTraces(tracePath, retainedTraces) || this.routeIntersectsRemainingLabels(tracePath, candidate)) {
15315
+ let collisionTraces = retainedTraces;
15316
+ if (candidate.recoveryMode === "routed_components") {
15317
+ collisionTraces = retainedTraces.filter(
15318
+ (trace) => trace.globalConnNetId !== candidate.firstLabel.globalConnNetId
15319
+ );
15320
+ }
15321
+ if (doesTraceOverlapWithExistingTraces(tracePath, collisionTraces) || this.routeIntersectsRemainingLabels(tracePath, candidate)) {
15196
15322
  return;
15197
15323
  }
15198
15324
  const [firstPin, secondPin] = candidate.pins;
@@ -15207,11 +15333,22 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15207
15333
  pinIds: [firstPin.pinId, secondPin.pinId]
15208
15334
  };
15209
15335
  this.outputTraces = [...retainedTraces, recoveredTrace];
15210
- this.outputNetLabelPlacements = this.outputNetLabelPlacements.filter(
15211
- (label) => label !== candidate.firstLabel && label !== candidate.secondLabel
15212
- );
15336
+ if (candidate.recoveryMode === "fallback_labels") {
15337
+ this.outputNetLabelPlacements = this.outputNetLabelPlacements.filter(
15338
+ (label) => label !== candidate.firstLabel && label !== candidate.secondLabel
15339
+ );
15340
+ }
15213
15341
  this.stats.recoveredTraceCount++;
15214
15342
  }
15343
+ areCandidatePinsAlreadyConnected(candidate) {
15344
+ if (!candidate.netConnectionPinIds) return false;
15345
+ return getTraceConnectedPinComponents({
15346
+ pinIds: candidate.netConnectionPinIds,
15347
+ traces: this.outputTraces
15348
+ }).some(
15349
+ (component) => component.pinIds.includes(candidate.pins[0].pinId) && component.pinIds.includes(candidate.pins[1].pinId)
15350
+ );
15351
+ }
15215
15352
  _step() {
15216
15353
  if (this.activeSubSolver) {
15217
15354
  this.activeSubSolver.step();
@@ -15226,7 +15363,7 @@ var NetLabelToTraceSolver = class extends BaseSolver {
15226
15363
  return;
15227
15364
  }
15228
15365
  let candidate = this.queuedCandidates.shift();
15229
- while (candidate && (!this.outputNetLabelPlacements.includes(candidate.firstLabel) || !this.outputNetLabelPlacements.includes(candidate.secondLabel))) {
15366
+ while (candidate && (!this.outputNetLabelPlacements.includes(candidate.firstLabel) || !this.outputNetLabelPlacements.includes(candidate.secondLabel) || this.areCandidatePinsAlreadyConnected(candidate))) {
15230
15367
  candidate = this.queuedCandidates.shift();
15231
15368
  }
15232
15369
  if (!candidate) {
@@ -12,6 +12,7 @@ import {
12
12
  getTraceRecoveryConnectivityMaps,
13
13
  type TraceRecoveryPin,
14
14
  } from "lib/solvers/NetLabelTraceRecovery/getTraceRecoveryConnectivityMaps"
15
+ import { getTraceConnectedPinComponents } from "lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents"
15
16
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
16
17
  import { SchematicTraceSingleLineSolver2 } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
17
18
  import type {
@@ -37,10 +38,14 @@ interface CandidatePair {
37
38
  perpendicularOffset: number
38
39
  routeDistance: number
39
40
  key: string
41
+ recoveryMode: "fallback_labels" | "routed_components"
42
+ netConnectionPinIds?: PinId[]
40
43
  }
41
44
 
42
45
  const AVAILABLE_NET_ORIENTATION_PREFIX = "available-net-orientation-"
43
46
  const RECOVERED_TRACE_PREFIX = "net-label-to-trace-"
47
+ const MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET = 0.05
48
+ const MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET = 0.25
44
49
 
45
50
  const getCanonicalPairKey = (firstPinId: PinId, secondPinId: PinId) =>
46
51
  [firstPinId, secondPinId].sort().join("--")
@@ -70,14 +75,28 @@ export const pathIntersectsRenderedLabel = (
70
75
  return false
71
76
  }
72
77
 
73
- const getPerpendicularOffset = (
78
+ const getPerpendicularOffset = (firstPoint: Point, secondPoint: Point) => {
79
+ const xDistance = Math.abs(firstPoint.x - secondPoint.x)
80
+ const yDistance = Math.abs(firstPoint.y - secondPoint.y)
81
+ if (xDistance >= yDistance) return yDistance
82
+ return xDistance
83
+ }
84
+
85
+ const arePinsCoFacingAlongSeparationAxis = (
74
86
  firstPin: TraceRecoveryPin,
75
87
  secondPin: TraceRecoveryPin,
76
88
  ) => {
89
+ if (firstPin._facingDirection !== secondPin._facingDirection) return false
77
90
  const xDistance = Math.abs(firstPin.x - secondPin.x)
78
91
  const yDistance = Math.abs(firstPin.y - secondPin.y)
79
- if (xDistance >= yDistance) return yDistance
80
- return xDistance
92
+ if (xDistance >= yDistance) {
93
+ return (
94
+ firstPin._facingDirection === "x+" || firstPin._facingDirection === "x-"
95
+ )
96
+ }
97
+ return (
98
+ firstPin._facingDirection === "y+" || firstPin._facingDirection === "y-"
99
+ )
81
100
  }
82
101
 
83
102
  export class NetLabelToTraceSolver extends BaseSolver {
@@ -113,20 +132,20 @@ export class NetLabelToTraceSolver extends BaseSolver {
113
132
  return [this.input]
114
133
  }
115
134
 
116
- private isEligiblePortOnlyDirectConnectionLabel(
135
+ private isPortOnlyFallbackLabel(
117
136
  label: NetLabelPlacement,
118
- groundGlobalConnNetId?: GlobalConnNetId,
137
+ groundGlobalConnNetIds: Set<GlobalConnNetId>,
119
138
  ) {
120
- if (
139
+ return !(
121
140
  label.pinIds.length !== 1 ||
122
141
  label.mspConnectionPairIds.length !== 0 ||
123
142
  !label.netId ||
124
- label.netId === "GND" ||
125
- label.globalConnNetId === groundGlobalConnNetId
126
- ) {
127
- return false
128
- }
143
+ groundGlobalConnNetIds.has(label.globalConnNetId)
144
+ )
145
+ }
129
146
 
147
+ private isDirectConnectionLabel(label: NetLabelPlacement) {
148
+ if (!label.netId || label.pinIds.length !== 1) return false
130
149
  const pinId = label.pinIds[0]!
131
150
  return this.inputProblem.directConnections.some(
132
151
  (connection) =>
@@ -134,20 +153,35 @@ export class NetLabelToTraceSolver extends BaseSolver {
134
153
  )
135
154
  }
136
155
 
156
+ private getMultiPinNetConnection(label: NetLabelPlacement) {
157
+ if (!label.netId || label.pinIds.length !== 1) return undefined
158
+ const pinId = label.pinIds[0]!
159
+ return this.inputProblem.netConnections.find(
160
+ (connection) =>
161
+ connection.isGround === false &&
162
+ connection.pinIds.length > 2 &&
163
+ connection.netId === label.netId &&
164
+ connection.pinIds.includes(pinId),
165
+ )
166
+ }
167
+
137
168
  private buildCandidatePairs() {
138
169
  const { netConnMap } = getConnectivityMapsFromInputProblem(
139
170
  this.inputProblem,
140
171
  )
141
- const groundGlobalConnNetId =
142
- netConnMap.getNetConnectedToId("GND") ?? undefined
172
+ const groundGlobalConnNetIds = new Set<GlobalConnNetId>()
173
+ for (const connection of this.inputProblem.netConnections) {
174
+ if (!connection.isGround) continue
175
+ const globalConnNetId = netConnMap.getNetConnectedToId(connection.netId)
176
+ if (globalConnNetId) groundGlobalConnNetIds.add(globalConnNetId)
177
+ }
143
178
  const labelsByGlobalNet = new Map<GlobalConnNetId, NetLabelPlacement[]>()
144
179
 
145
180
  for (const label of this.input.netLabelPlacements) {
146
181
  if (
147
- !this.isEligiblePortOnlyDirectConnectionLabel(
148
- label,
149
- groundGlobalConnNetId,
150
- )
182
+ !this.isPortOnlyFallbackLabel(label, groundGlobalConnNetIds) ||
183
+ (!this.isDirectConnectionLabel(label) &&
184
+ !this.getMultiPinNetConnection(label))
151
185
  ) {
152
186
  continue
153
187
  }
@@ -169,6 +203,25 @@ export class NetLabelToTraceSolver extends BaseSolver {
169
203
  const firstPin = this.pinMap.get(firstLabel.pinIds[0]!)
170
204
  const secondPin = this.pinMap.get(secondLabel.pinIds[0]!)
171
205
  if (!firstPin || !secondPin) continue
206
+ const perpendicularOffset = getPerpendicularOffset(
207
+ firstPin,
208
+ secondPin,
209
+ )
210
+ const bothLabelsBelongToDirectConnections =
211
+ this.isDirectConnectionLabel(firstLabel) &&
212
+ this.isDirectConnectionLabel(secondLabel)
213
+ const firstMultiPinNetConnection =
214
+ this.getMultiPinNetConnection(firstLabel)
215
+ const secondMultiPinNetConnection =
216
+ this.getMultiPinNetConnection(secondLabel)
217
+ if (
218
+ !bothLabelsBelongToDirectConnections &&
219
+ (!firstMultiPinNetConnection ||
220
+ firstMultiPinNetConnection !== secondMultiPinNetConnection ||
221
+ perpendicularOffset > MAX_NAMED_NET_RECOVERY_PERPENDICULAR_OFFSET)
222
+ ) {
223
+ continue
224
+ }
172
225
  if (
173
226
  arePinsInDifferentSchematicSections(
174
227
  this.inputProblem,
@@ -194,16 +247,19 @@ export class NetLabelToTraceSolver extends BaseSolver {
194
247
  firstLabel,
195
248
  secondLabel,
196
249
  pins: [firstPin, secondPin],
197
- perpendicularOffset: getPerpendicularOffset(firstPin, secondPin),
250
+ perpendicularOffset,
198
251
  routeDistance:
199
252
  Math.abs(firstPin.x - secondPin.x) +
200
253
  Math.abs(firstPin.y - secondPin.y),
201
254
  key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
255
+ recoveryMode: "fallback_labels",
202
256
  })
203
257
  }
204
258
  }
205
259
  }
206
260
 
261
+ candidates.push(...this.buildRoutedComponentCandidates())
262
+
207
263
  candidates.sort(
208
264
  (first, second) =>
209
265
  first.perpendicularOffset - second.perpendicularOffset ||
@@ -213,6 +269,129 @@ export class NetLabelToTraceSolver extends BaseSolver {
213
269
  return candidates
214
270
  }
215
271
 
272
+ private buildRoutedComponentCandidates() {
273
+ const { netConnMap } = getConnectivityMapsFromInputProblem(
274
+ this.inputProblem,
275
+ )
276
+ const candidates: CandidatePair[] = []
277
+
278
+ for (const connection of this.inputProblem.netConnections) {
279
+ if (connection.pinIds.length <= 2 || connection.isGround !== false)
280
+ continue
281
+ const globalConnNetId = netConnMap.getNetConnectedToId(connection.netId)
282
+ if (!globalConnNetId) continue
283
+
284
+ const traceConnectedPinComponents = getTraceConnectedPinComponents({
285
+ pinIds: connection.pinIds,
286
+ traces: this.outputTraces.filter(
287
+ (trace) => trace.globalConnNetId === globalConnNetId,
288
+ ),
289
+ }).filter((component) => component.traces.length > 0)
290
+
291
+ for (
292
+ let firstIndex = 0;
293
+ firstIndex < traceConnectedPinComponents.length;
294
+ firstIndex++
295
+ ) {
296
+ for (
297
+ let secondIndex = firstIndex + 1;
298
+ secondIndex < traceConnectedPinComponents.length;
299
+ secondIndex++
300
+ ) {
301
+ const firstComponent = traceConnectedPinComponents[firstIndex]!
302
+ const secondComponent = traceConnectedPinComponents[secondIndex]!
303
+ const firstLabel = this.input.netLabelPlacements.find(
304
+ (label) =>
305
+ label.globalConnNetId === globalConnNetId &&
306
+ label.mspConnectionPairIds.length > 0 &&
307
+ label.pinIds.some((pinId) =>
308
+ firstComponent.pinIds.includes(pinId),
309
+ ),
310
+ )
311
+ const secondLabel = this.input.netLabelPlacements.find(
312
+ (label) =>
313
+ label.globalConnNetId === globalConnNetId &&
314
+ label.mspConnectionPairIds.length > 0 &&
315
+ label.pinIds.some((pinId) =>
316
+ secondComponent.pinIds.includes(pinId),
317
+ ),
318
+ )
319
+ if (!firstLabel || !secondLabel || firstLabel === secondLabel)
320
+ continue
321
+ if (
322
+ getPerpendicularOffset(
323
+ firstLabel.anchorPoint,
324
+ secondLabel.anchorPoint,
325
+ ) > MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET
326
+ ) {
327
+ continue
328
+ }
329
+
330
+ let bestCandidate: CandidatePair | undefined
331
+ for (const firstPinId of firstComponent.pinIds) {
332
+ for (const secondPinId of secondComponent.pinIds) {
333
+ const firstPin = this.pinMap.get(firstPinId)
334
+ const secondPin = this.pinMap.get(secondPinId)
335
+ if (!firstPin || !secondPin) continue
336
+ const perpendicularOffset = getPerpendicularOffset(
337
+ firstPin,
338
+ secondPin,
339
+ )
340
+ if (
341
+ perpendicularOffset >
342
+ MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET ||
343
+ arePinsCoFacingAlongSeparationAxis(firstPin, secondPin) ||
344
+ arePinsInDifferentSchematicSections(
345
+ this.inputProblem,
346
+ firstPin,
347
+ secondPin,
348
+ ) ||
349
+ doesPairCrossRestrictedCenterLines({
350
+ inputProblem: this.inputProblem,
351
+ chipMap: this.chipMap,
352
+ pinIdMap: this.pinMap,
353
+ p1: firstPin,
354
+ p2: secondPin,
355
+ })
356
+ ) {
357
+ continue
358
+ }
359
+
360
+ const routeDistance =
361
+ Math.abs(firstPin.x - secondPin.x) +
362
+ Math.abs(firstPin.y - secondPin.y)
363
+ const candidate: CandidatePair = {
364
+ firstLabel,
365
+ secondLabel,
366
+ pins: [firstPin, secondPin],
367
+ perpendicularOffset,
368
+ routeDistance,
369
+ key: getCanonicalPairKey(firstPin.pinId, secondPin.pinId),
370
+ recoveryMode: "routed_components",
371
+ netConnectionPinIds: connection.pinIds,
372
+ }
373
+ if (
374
+ !bestCandidate ||
375
+ candidate.routeDistance < bestCandidate.routeDistance ||
376
+ (candidate.routeDistance === bestCandidate.routeDistance &&
377
+ (candidate.perpendicularOffset <
378
+ bestCandidate.perpendicularOffset ||
379
+ (candidate.perpendicularOffset ===
380
+ bestCandidate.perpendicularOffset &&
381
+ candidate.key.localeCompare(bestCandidate.key) < 0)))
382
+ ) {
383
+ bestCandidate = candidate
384
+ }
385
+ }
386
+ }
387
+ if (bestCandidate) candidates.push(bestCandidate)
388
+ }
389
+ }
390
+ }
391
+
392
+ return candidates
393
+ }
394
+
216
395
  private isSupersededConnectorTrace(
217
396
  trace: SolvedTracePath,
218
397
  candidate: CandidatePair,
@@ -247,8 +426,15 @@ export class NetLabelToTraceSolver extends BaseSolver {
247
426
  const retainedTraces = this.outputTraces.filter(
248
427
  (trace) => !this.isSupersededConnectorTrace(trace, candidate),
249
428
  )
429
+ let collisionTraces = retainedTraces
430
+ if (candidate.recoveryMode === "routed_components") {
431
+ collisionTraces = retainedTraces.filter(
432
+ (trace) =>
433
+ trace.globalConnNetId !== candidate.firstLabel.globalConnNetId,
434
+ )
435
+ }
250
436
  if (
251
- doesTraceOverlapWithExistingTraces(tracePath, retainedTraces) ||
437
+ doesTraceOverlapWithExistingTraces(tracePath, collisionTraces) ||
252
438
  this.routeIntersectsRemainingLabels(tracePath, candidate)
253
439
  ) {
254
440
  return
@@ -267,13 +453,27 @@ export class NetLabelToTraceSolver extends BaseSolver {
267
453
  }
268
454
 
269
455
  this.outputTraces = [...retainedTraces, recoveredTrace]
270
- this.outputNetLabelPlacements = this.outputNetLabelPlacements.filter(
271
- (label) =>
272
- label !== candidate.firstLabel && label !== candidate.secondLabel,
273
- )
456
+ if (candidate.recoveryMode === "fallback_labels") {
457
+ this.outputNetLabelPlacements = this.outputNetLabelPlacements.filter(
458
+ (label) =>
459
+ label !== candidate.firstLabel && label !== candidate.secondLabel,
460
+ )
461
+ }
274
462
  this.stats.recoveredTraceCount++
275
463
  }
276
464
 
465
+ private areCandidatePinsAlreadyConnected(candidate: CandidatePair) {
466
+ if (!candidate.netConnectionPinIds) return false
467
+ return getTraceConnectedPinComponents({
468
+ pinIds: candidate.netConnectionPinIds,
469
+ traces: this.outputTraces,
470
+ }).some(
471
+ (component) =>
472
+ component.pinIds.includes(candidate.pins[0].pinId) &&
473
+ component.pinIds.includes(candidate.pins[1].pinId),
474
+ )
475
+ }
476
+
277
477
  override _step() {
278
478
  if (this.activeSubSolver) {
279
479
  this.activeSubSolver.step()
@@ -292,7 +492,8 @@ export class NetLabelToTraceSolver extends BaseSolver {
292
492
  while (
293
493
  candidate &&
294
494
  (!this.outputNetLabelPlacements.includes(candidate.firstLabel) ||
295
- !this.outputNetLabelPlacements.includes(candidate.secondLabel))
495
+ !this.outputNetLabelPlacements.includes(candidate.secondLabel) ||
496
+ this.areCandidatePinsAlreadyConnected(candidate))
296
497
  ) {
297
498
  candidate = this.queuedCandidates.shift()
298
499
  }
@@ -325,15 +325,26 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
325
325
  excludeRectsForSegment: (segIndex) => {
326
326
  const lastSegIndex = path.length - 2
327
327
  const excludedRects = new Set<ObstacleRect>()
328
+ const segmentStart = path[segIndex]!
329
+ const segmentEnd = path[segIndex + 1]!
328
330
 
329
- if (segIndex === 0 || segIndex === lastSegIndex) {
331
+ const oppositeTerminalPin =
332
+ segIndex === 0 ? PB : segIndex === lastSegIndex ? PA : null
333
+ if (oppositeTerminalPin) {
330
334
  for (const textObstacle of this.textObstacles) {
331
- excludedRects.add(textObstacle)
335
+ // Preserve the existing terminal-segment clearance exception, but
336
+ // do not let a segment that wraps around the opposite endpoint cut
337
+ // through that component's actual text.
338
+ const textBounds = getTextBoxBounds(textObstacle.textBox)
339
+ if (
340
+ textObstacle.textBox.chipId !== oppositeTerminalPin.chipId ||
341
+ !segmentIntersectsRect(segmentStart, segmentEnd, textBounds)
342
+ ) {
343
+ excludedRects.add(textObstacle)
344
+ }
332
345
  }
333
346
  }
334
347
 
335
- const segmentStart = path[segIndex]!
336
- const segmentEnd = path[segIndex + 1]!
337
348
  const endpointEpsilon = 1e-9
338
349
  const segmentTouchesPin = (pin: MspConnectionPair["pins"][number]) =>
339
350
  [segmentStart, segmentEnd].some(