@tscircuit/schematic-trace-solver 0.0.130 → 0.0.132

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.
@@ -10,6 +10,7 @@ import {
10
10
  } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
11
11
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
12
12
  import type {
13
+ ChipId,
13
14
  InputNetConnection,
14
15
  InputPin,
15
16
  InputProblem,
@@ -54,6 +55,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
54
55
  currentLabelIndex: number | null = null
55
56
  currentLabel: NetLabelPlacement | null = null
56
57
  currentCandidateResults: EvaluatedCandidate[] = []
58
+ private crowdedPortOnlyLabelIndices = new Set<number>()
57
59
 
58
60
  blockedStandaloneLabelIndex: number | null = null
59
61
  blockedStandaloneLabelTargetAnchorX: number | null = null
@@ -76,6 +78,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
76
78
  params.inputProblem._chipObstacleSpatialIndex ??
77
79
  new ChipObstacleSpatialIndex(params.inputProblem.chips)
78
80
  this.maxSearchDistance = getMaxSearchDistance(params.inputProblem)
81
+ this.crowdedPortOnlyLabelIndices = this.getCrowdedPortOnlyLabelIndices()
79
82
  this.queuedLabelIndices = this.getProcessableLabelIndices()
80
83
  this.setCurrentLabel(this.queuedLabelIndices[0] ?? null)
81
84
  }
@@ -111,10 +114,11 @@ export class AvailableNetOrientationSolver extends BaseSolver {
111
114
  const indices: number[] = []
112
115
 
113
116
  for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
114
- if (this.shouldProcessLabel(this.outputNetLabelPlacements[i]!)) {
117
+ if (this.shouldProcessLabel(this.outputNetLabelPlacements[i]!, i)) {
115
118
  indices.push(i)
116
119
  }
117
120
  }
121
+ this.sortCrowdedTopBankByPin(indices)
118
122
 
119
123
  const blockedStandaloneLabelIndex = indices.find((index) => {
120
124
  const label = this.outputNetLabelPlacements[index]!
@@ -184,9 +188,98 @@ export class AvailableNetOrientationSolver extends BaseSolver {
184
188
  return indices
185
189
  }
186
190
 
187
- private shouldProcessLabel(label: NetLabelPlacement) {
191
+ private sortCrowdedTopBankByPin(indices: number[]) {
192
+ const slotsByChip = new Map<ChipId, number[]>()
193
+
194
+ for (let slot = 0; slot < indices.length; slot++) {
195
+ const labelIndex = indices[slot]!
196
+ if (!this.crowdedPortOnlyLabelIndices.has(labelIndex)) continue
197
+ const label = this.outputNetLabelPlacements[labelIndex]!
198
+ if (this.getChipSideForPoint(label.anchorPoint) !== "top") continue
199
+ const pin = this.pinMap[label.pinIds[0]!]
200
+ if (!pin) continue
201
+ const slots = slotsByChip.get(pin.chipId) ?? []
202
+ slots.push(slot)
203
+ slotsByChip.set(pin.chipId, slots)
204
+ }
205
+
206
+ for (const slots of slotsByChip.values()) {
207
+ const labelIndices = slots
208
+ .map((slot) => indices[slot]!)
209
+ .sort((a, b) => {
210
+ const aPin = this.pinMap[this.outputNetLabelPlacements[a]!.pinIds[0]!]
211
+ const bPin = this.pinMap[this.outputNetLabelPlacements[b]!.pinIds[0]!]
212
+ return aPin!.x - bPin!.x
213
+ })
214
+ for (let position = 0; position < slots.length; position++) {
215
+ indices[slots[position]!] = labelIndices[position]!
216
+ }
217
+ }
218
+ }
219
+
220
+ private shouldProcessLabel(label: NetLabelPlacement, labelIndex: number) {
188
221
  const orientations = this.getAvailableOrientations(label)
189
- return orientations.length > 0 && !orientations.includes(label.orientation)
222
+ if (orientations.length === 0) return false
223
+ if (!orientations.includes(label.orientation)) return true
224
+ if (!this.crowdedPortOnlyLabelIndices.has(labelIndex)) return false
225
+
226
+ const bounds = getRectBounds(label.center, label.width, label.height)
227
+ return this.outputNetLabelPlacements.some((otherLabel, otherIndex) => {
228
+ if (otherIndex === labelIndex) return false
229
+ if (otherLabel.globalConnNetId === label.globalConnNetId) return false
230
+ return rectsOverlap(
231
+ bounds,
232
+ getRectBounds(otherLabel.center, otherLabel.width, otherLabel.height),
233
+ )
234
+ })
235
+ }
236
+
237
+ private getCrowdedPortOnlyLabelIndices() {
238
+ const labelIndicesByChipAndSide = new Map<ChipId, Map<ChipSide, number[]>>()
239
+ for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
240
+ const label = this.outputNetLabelPlacements[i]!
241
+ if (!this.isPortOnlyLabel(label)) continue
242
+ if (!this.isStandaloneSinglePinNetLabel(label)) continue
243
+ if (this.getAvailableOrientations(label).length === 0) continue
244
+ const pin = this.pinMap[label.pinIds[0]!]
245
+ if (!pin) continue
246
+ const chipSide = this.getChipSideForPoint(label.anchorPoint)
247
+ if (!chipSide) continue
248
+ const labelIndicesBySide =
249
+ labelIndicesByChipAndSide.get(pin.chipId) ??
250
+ new Map<ChipSide, number[]>()
251
+ const labelIndices = labelIndicesBySide.get(chipSide) ?? []
252
+ labelIndices.push(i)
253
+ labelIndicesBySide.set(chipSide, labelIndices)
254
+ labelIndicesByChipAndSide.set(pin.chipId, labelIndicesBySide)
255
+ }
256
+
257
+ const crowdedIndices = new Set<number>()
258
+ for (const labelIndicesBySide of labelIndicesByChipAndSide.values()) {
259
+ for (const labelIndices of labelIndicesBySide.values()) {
260
+ const hasWrongOrientation = labelIndices.some((index) => {
261
+ const label = this.outputNetLabelPlacements[index]!
262
+ return !this.getAvailableOrientations(label).includes(
263
+ label.orientation,
264
+ )
265
+ })
266
+ const hasOverlap = labelIndices.some((index, position) => {
267
+ const label = this.outputNetLabelPlacements[index]!
268
+ const bounds = getRectBounds(label.center, label.width, label.height)
269
+ return labelIndices.slice(position + 1).some((otherIndex) => {
270
+ const other = this.outputNetLabelPlacements[otherIndex]!
271
+ return rectsOverlap(
272
+ bounds,
273
+ getRectBounds(other.center, other.width, other.height),
274
+ )
275
+ })
276
+ })
277
+ if (!hasWrongOrientation || !hasOverlap) continue
278
+ for (const index of labelIndices) crowdedIndices.add(index)
279
+ }
280
+ }
281
+
282
+ return crowdedIndices
190
283
  }
191
284
 
192
285
  private processLabel(labelIndex: number) {
@@ -219,7 +312,11 @@ export class AvailableNetOrientationSolver extends BaseSolver {
219
312
  ) {
220
313
  if (candidate.phase === "trace-anchor") return
221
314
 
222
- const tracePath = this.getCandidateConnectorTrace(label, candidate)
315
+ const tracePath = this.getCandidateConnectorTrace(
316
+ label,
317
+ candidate,
318
+ labelIndex,
319
+ )
223
320
  if (tracePath.length < 2) return
224
321
 
225
322
  const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}`
@@ -254,6 +351,21 @@ export class AvailableNetOrientationSolver extends BaseSolver {
254
351
  private findCorrectedCandidate(label: NetLabelPlacement, labelIndex: number) {
255
352
  const orientations = this.getAvailableOrientations(label)
256
353
  const requiredOrientation = orientations[0]!
354
+
355
+ if (
356
+ this.crowdedPortOnlyLabelIndices.has(labelIndex) &&
357
+ this.getChipSideForPoint(label.anchorPoint) === "top" &&
358
+ isYOrientation(requiredOrientation)
359
+ ) {
360
+ const topFanoutCandidate =
361
+ this.findValidCrowdedTopVerticalFanoutCandidate(
362
+ label,
363
+ labelIndex,
364
+ requiredOrientation,
365
+ )
366
+ if (topFanoutCandidate) return topFanoutCandidate
367
+ }
368
+
257
369
  const netConnection = this.inputProblem.netConnections.find(
258
370
  (connection) => connection.netId === label.netId,
259
371
  )
@@ -334,6 +446,117 @@ export class AvailableNetOrientationSolver extends BaseSolver {
334
446
  )
335
447
  }
336
448
 
449
+ private findValidCrowdedTopVerticalFanoutCandidate(
450
+ label: NetLabelPlacement,
451
+ labelIndex: number,
452
+ orientation: "y+" | "y-",
453
+ ) {
454
+ const pin = this.pinMap[label.pinIds[0]!]
455
+ const chip = pin
456
+ ? this.chipObstacleSpatialIndex.chips.find(
457
+ (candidate) => candidate.chipId === pin.chipId,
458
+ )
459
+ : undefined
460
+ if (!chip) return null
461
+
462
+ const { width, height } = getDimsForOrientation({
463
+ orientation,
464
+ netLabelWidth: this.getNetLabelWidth(label),
465
+ netLabelHeight: this.getNetLabelHeight(label),
466
+ })
467
+
468
+ const nextPinToRight = this.outputNetLabelPlacements
469
+ .map((otherLabel, index) => ({
470
+ index,
471
+ pin: this.pinMap[otherLabel.pinIds[0]!],
472
+ }))
473
+ .filter(
474
+ ({ index, pin: otherPin }) =>
475
+ this.crowdedPortOnlyLabelIndices.has(index) &&
476
+ otherPin?.chipId === pin.chipId &&
477
+ otherPin.x > pin.x + EPS,
478
+ )
479
+ .sort((a, b) => a.pin!.x - b.pin!.x)[0]?.pin
480
+
481
+ // A down-facing label uses the terminal right-hand escape. Interior
482
+ // down-facing labels fall back to the general search instead of crossing
483
+ // the remaining pin corridors.
484
+ if (orientation === "y-" && nextPinToRight) return null
485
+
486
+ const anchorX =
487
+ orientation === "y-"
488
+ ? Math.max(
489
+ label.anchorPoint.x + LABEL_SEARCH_STEP,
490
+ chip.bounds.maxX + width / 2 + LABEL_TRACE_CLEARANCE,
491
+ )
492
+ : Math.min(
493
+ label.anchorPoint.x,
494
+ (nextPinToRight?.x ?? chip.bounds.maxX) -
495
+ LABEL_SEARCH_STEP -
496
+ width / 2,
497
+ )
498
+
499
+ let highestPlacedLabelY = chip.bounds.maxY
500
+ let lowestUpFacingLabelY = Number.POSITIVE_INFINITY
501
+ let highestUpFacingLabelY = Number.NEGATIVE_INFINITY
502
+ for (let index = 0; index < this.outputNetLabelPlacements.length; index++) {
503
+ if (index === labelIndex) continue
504
+ if (!this.crowdedPortOnlyLabelIndices.has(index)) continue
505
+ if (this.queuedLabelIndices.includes(index)) continue
506
+ const placedLabel = this.outputNetLabelPlacements[index]!
507
+ const placedBounds = getRectBounds(
508
+ placedLabel.center,
509
+ placedLabel.width,
510
+ placedLabel.height,
511
+ )
512
+ highestPlacedLabelY = Math.max(highestPlacedLabelY, placedBounds.maxY)
513
+ if (placedLabel.orientation !== "y+") continue
514
+ lowestUpFacingLabelY = Math.min(lowestUpFacingLabelY, placedBounds.minY)
515
+ highestUpFacingLabelY = Math.max(highestUpFacingLabelY, placedBounds.maxY)
516
+ }
517
+
518
+ const stackedAnchorY =
519
+ highestPlacedLabelY +
520
+ LABEL_SEARCH_STEP +
521
+ (orientation === "y-" ? height : 0)
522
+ const hasUpFacingStack =
523
+ Number.isFinite(lowestUpFacingLabelY) &&
524
+ Number.isFinite(highestUpFacingLabelY)
525
+ const centeredAnchorY = hasUpFacingStack
526
+ ? (lowestUpFacingLabelY + highestUpFacingLabelY) / 2
527
+ : stackedAnchorY
528
+ const anchorYs =
529
+ orientation === "y-"
530
+ ? [centeredAnchorY, stackedAnchorY]
531
+ : [stackedAnchorY]
532
+
533
+ for (const anchorY of anchorYs) {
534
+ if (anchorY - label.anchorPoint.y > this.maxSearchDistance + EPS) {
535
+ continue
536
+ }
537
+ const candidate = this.createCandidate(
538
+ label,
539
+ { x: anchorX, y: anchorY },
540
+ orientation,
541
+ )
542
+ const result = this.evaluateCandidate(
543
+ candidate,
544
+ label,
545
+ labelIndex,
546
+ "lateral-shift",
547
+ anchorY - label.anchorPoint.y,
548
+ anchorX - label.anchorPoint.x,
549
+ )
550
+ this.currentCandidateResults.push(result)
551
+ if (result.status !== "valid") continue
552
+
553
+ result.selected = true
554
+ return result
555
+ }
556
+
557
+ return null
558
+ }
559
+
337
560
  private hasTraceContinuingInOrientation(
338
561
  label: NetLabelPlacement,
339
562
  orientation: "y+" | "y-",
@@ -703,12 +926,35 @@ export class AvailableNetOrientationSolver extends BaseSolver {
703
926
  EvaluatedCandidate,
704
927
  "anchorPoint" | "connectorSource" | "orientation" | "phase"
705
928
  >,
929
+ labelIndex: number,
706
930
  ) {
931
+ if (
932
+ this.crowdedPortOnlyLabelIndices.has(labelIndex) &&
933
+ this.getChipSideForPoint(label.anchorPoint) === "top" &&
934
+ isYOrientation(candidate.orientation)
935
+ ) {
936
+ // Top-edge vertical labels use the target row as their bend row. This
937
+ // makes every connector leave the pin upward first, instead of creating
938
+ // a low horizontal bus that the overlap solver later pushes below the
939
+ // component. It also gives a y- ground label the desired up-then-right
940
+ // connector while preserving its required facing direction.
941
+ return simplifyOrthogonalPath([
942
+ label.anchorPoint,
943
+ { x: label.anchorPoint.x, y: candidate.anchorPoint.y },
944
+ candidate.anchorPoint,
945
+ ])
946
+ }
947
+
707
948
  if (candidate.phase === "lateral-shift") {
708
- const orientDir = dir(candidate.orientation)
949
+ const chipOutwardDir = this.crowdedPortOnlyLabelIndices.has(labelIndex)
950
+ ? this.getChipOutwardDirection(label.anchorPoint)
951
+ : (() => {
952
+ const orientDir = dir(candidate.orientation)
953
+ return { x: -orientDir.x, y: -orientDir.y }
954
+ })()
709
955
  const kickedSource = {
710
- x: label.anchorPoint.x - orientDir.x * LABEL_SEARCH_STEP,
711
- y: label.anchorPoint.y - orientDir.y * LABEL_SEARCH_STEP,
956
+ x: label.anchorPoint.x + chipOutwardDir.x * LABEL_SEARCH_STEP,
957
+ y: label.anchorPoint.y + chipOutwardDir.y * LABEL_SEARCH_STEP,
712
958
  }
713
959
  return simplifyOrthogonalPath([
714
960
  label.anchorPoint,
@@ -1007,11 +1253,15 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1007
1253
  if (nonChipBoundsStatus !== "valid") return nonChipBoundsStatus
1008
1254
  }
1009
1255
 
1010
- const connectorTrace = this.getCandidateConnectorTrace(label, {
1011
- anchorPoint: candidate.anchorPoint,
1012
- orientation: candidate.orientation,
1013
- phase,
1014
- })
1256
+ const connectorTrace = this.getCandidateConnectorTrace(
1257
+ label,
1258
+ {
1259
+ anchorPoint: candidate.anchorPoint,
1260
+ orientation: candidate.orientation,
1261
+ phase,
1262
+ },
1263
+ labelIndex,
1264
+ )
1015
1265
 
1016
1266
  for (const chip of this.chipObstacleSpatialIndex.chips) {
1017
1267
  if (tracePathCrossesAnyBounds(connectorTrace, chip.bounds)) {
@@ -1021,6 +1271,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1021
1271
 
1022
1272
  for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
1023
1273
  if (i === labelIndex) continue
1274
+ if (this.shouldIgnorePendingCrowdedLabel(labelIndex, i)) continue
1024
1275
  const otherLabel = this.outputNetLabelPlacements[i]!
1025
1276
  if (
1026
1277
  tracePathIntersectsBounds(
@@ -1227,9 +1478,21 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1227
1478
  return label.mspConnectionPairIds.length === 0
1228
1479
  }
1229
1480
 
1481
+ private shouldIgnorePendingCrowdedLabel(
1482
+ labelIndex: number,
1483
+ otherLabelIndex: number,
1484
+ ) {
1485
+ return (
1486
+ this.crowdedPortOnlyLabelIndices.has(labelIndex) &&
1487
+ this.crowdedPortOnlyLabelIndices.has(otherLabelIndex) &&
1488
+ this.queuedLabelIndices.includes(otherLabelIndex)
1489
+ )
1490
+ }
1491
+
1230
1492
  private intersectsAnyOtherNetLabel(bounds: Bounds, labelIndex: number) {
1231
1493
  for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
1232
1494
  if (i === labelIndex) continue
1495
+ if (this.shouldIgnorePendingCrowdedLabel(labelIndex, i)) continue
1233
1496
  const label = this.outputNetLabelPlacements[i]!
1234
1497
  const otherBounds = getRectBounds(label.center, label.width, label.height)
1235
1498
  if (
@@ -1312,6 +1575,21 @@ export class AvailableNetOrientationSolver extends BaseSolver {
1312
1575
  return { x: 0, y: 0 }
1313
1576
  }
1314
1577
 
1578
+ private getChipOutwardDirection(point: Point) {
1579
+ switch (this.getChipSideForPoint(point)) {
1580
+ case "left":
1581
+ return { x: -1, y: 0 }
1582
+ case "right":
1583
+ return { x: 1, y: 0 }
1584
+ case "bottom":
1585
+ return { x: 0, y: -1 }
1586
+ case "top":
1587
+ return { x: 0, y: 1 }
1588
+ default:
1589
+ return { x: 0, y: 0 }
1590
+ }
1591
+ }
1592
+
1315
1593
  private getContainingChipSide(point: Point) {
1316
1594
  let nearestSide: ChipSide | null = null
1317
1595
  let nearestDistance = Number.POSITIVE_INFINITY
@@ -4,6 +4,7 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
4
4
  import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
5
5
  import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
6
6
  import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
7
+ import { preservesLabelAnchors } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors"
7
8
  import {
8
9
  getVisibleTraceLength,
9
10
  getVisibleTraceSegmentCount,
@@ -164,6 +165,14 @@ const candidateIsClear = ({
164
165
  return false
165
166
  }
166
167
 
168
+ const candidateTraces = traces.map((trace) => {
169
+ if (trace.mspPairId === originalTrace.mspPairId) return candidateTrace
170
+ return trace
171
+ })
172
+ if (!preservesLabelAnchors(netLabelPlacements, traces, candidateTraces)) {
173
+ return false
174
+ }
175
+
167
176
  const otherNetLabelPlacements = netLabelPlacements.filter(
168
177
  (label) => label.globalConnNetId !== candidateTrace.globalConnNetId,
169
178
  )
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.130",
4
+ "version": "0.0.132",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",