@tscircuit/schematic-trace-solver 0.0.57 → 0.0.61

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.
Files changed (29) hide show
  1. package/dist/index.d.ts +15 -1
  2. package/dist/index.js +367 -136
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +150 -14
  4. package/lib/solvers/AvailableNetOrientationSolver/geometry.ts +1 -1
  5. package/lib/solvers/AvailableNetOrientationSolver/types.ts +1 -1
  6. package/lib/solvers/Example28Solver/reroute.ts +137 -17
  7. package/lib/solvers/Example28Solver/types.ts +1 -0
  8. package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +32 -10
  9. package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/candidates.ts +17 -5
  10. package/lib/solvers/TraceCleanupSolver/is4PointRectangle.ts +7 -2
  11. package/lib/types/InputProblem.ts +1 -0
  12. package/package.json +1 -1
  13. package/site/examples/example35.page.tsx +4 -0
  14. package/site/examples/example36.page.tsx +4 -0
  15. package/site/examples/example37.page.tsx +4 -0
  16. package/site/examples/example38.page.tsx +4 -0
  17. package/tests/assets/example35.json +127 -0
  18. package/tests/assets/example36.json +124 -0
  19. package/tests/assets/example37.json +173 -0
  20. package/tests/assets/example38.json +32 -0
  21. package/tests/examples/__snapshots__/example04.snap.svg +16 -17
  22. package/tests/examples/__snapshots__/example35.snap.svg +177 -0
  23. package/tests/examples/__snapshots__/example36.snap.svg +160 -0
  24. package/tests/examples/__snapshots__/example37.snap.svg +320 -0
  25. package/tests/examples/__snapshots__/example38.snap.svg +77 -0
  26. package/tests/examples/example35.test.ts +12 -0
  27. package/tests/examples/example36.test.ts +12 -0
  28. package/tests/examples/example37.test.ts +12 -0
  29. package/tests/examples/example38.test.ts +12 -0
@@ -20,6 +20,7 @@ import {
20
20
  isYOrientation,
21
21
  rangesOverlap,
22
22
  rectsOverlap,
23
+ simplifyOrthogonalPath,
23
24
  traceCrossesBoundsInterior,
24
25
  tracePathCrossesAnyBounds,
25
26
  tracePathCrossesAnyTrace,
@@ -127,7 +128,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
127
128
 
128
129
  private applyCandidate(
129
130
  label: NetLabelPlacement,
130
- candidate: CandidateLabel,
131
+ candidate: EvaluatedCandidate,
131
132
  labelIndex: number,
132
133
  ) {
133
134
  this.outputNetLabelPlacements[labelIndex] = {
@@ -139,14 +140,33 @@ export class AvailableNetOrientationSolver extends BaseSolver {
139
140
 
140
141
  private addConnectorTrace(
141
142
  label: NetLabelPlacement,
142
- candidate: CandidateLabel,
143
+ candidate: EvaluatedCandidate,
143
144
  labelIndex: number,
144
145
  ) {
145
- const tracePath = getConnectorTracePath(
146
- label.anchorPoint,
147
- candidate.anchorPoint,
148
- candidate.orientation,
149
- )
146
+ let tracePath: Point[]
147
+
148
+ if (candidate.phase === "lateral-shift") {
149
+ const orientDir = dir(candidate.orientation)
150
+ const kickedSource = {
151
+ x: label.anchorPoint.x - orientDir.x * LABEL_SEARCH_STEP,
152
+ y: label.anchorPoint.y - orientDir.y * LABEL_SEARCH_STEP,
153
+ }
154
+ tracePath = simplifyOrthogonalPath([
155
+ label.anchorPoint,
156
+ ...getConnectorTracePath(
157
+ kickedSource,
158
+ candidate.anchorPoint,
159
+ candidate.orientation,
160
+ ),
161
+ ])
162
+ } else {
163
+ tracePath = getConnectorTracePath(
164
+ label.anchorPoint,
165
+ candidate.anchorPoint,
166
+ candidate.orientation,
167
+ )
168
+ }
169
+
150
170
  if (tracePath.length < 2) return
151
171
 
152
172
  const mspPairId = `available-net-orientation-${labelIndex}-${label.netId ?? label.globalConnNetId}`
@@ -185,10 +205,19 @@ export class AvailableNetOrientationSolver extends BaseSolver {
185
205
  labelIndex,
186
206
  orientations,
187
207
  )
208
+ if (rotatedCandidate) return rotatedCandidate
209
+
210
+ const shiftedCandidate = this.findValidShiftedCandidate(
211
+ label,
212
+ orientations[0]!,
213
+ labelIndex,
214
+ )
215
+ if (shiftedCandidate) return shiftedCandidate
188
216
 
189
- return (
190
- rotatedCandidate ??
191
- this.findValidShiftedCandidate(label, orientations[0]!, labelIndex)
217
+ return this.findValidLateralShiftedCandidate(
218
+ label,
219
+ orientations[0]!,
220
+ labelIndex,
192
221
  )
193
222
  }
194
223
 
@@ -266,6 +295,63 @@ export class AvailableNetOrientationSolver extends BaseSolver {
266
295
  return null
267
296
  }
268
297
 
298
+ /**
299
+ * When all candidates fail for the current (unshifted) position, try
300
+ * shifting the label anchor laterally — x for y-orientations, y for
301
+ * x-orientations — and re-attempting the required orientation.
302
+ *
303
+ * Offsets are tried in alternating sign order:
304
+ * -1·step, +1·step, -2·step, +2·step, …
305
+ * so the nearest escape routes are tested first.
306
+ */
307
+ private findValidLateralShiftedCandidate(
308
+ label: NetLabelPlacement,
309
+ orientation: FacingDirection,
310
+ labelIndex: number,
311
+ ): EvaluatedCandidate | null {
312
+ const direction = dir(orientation)
313
+ const initialBaseAnchor = this.getSearchStartAnchor(label, orientation)
314
+
315
+ // Lateral axis: perpendicular to the orientation direction
316
+ const lateralDir: Point = {
317
+ x: isYOrientation(orientation) ? 1 : 0,
318
+ y: isXOrientation(orientation) ? 1 : 0,
319
+ }
320
+
321
+ const maxSteps = Math.ceil(this.maxSearchDistance / LABEL_SEARCH_STEP)
322
+
323
+ for (let step = 1; step <= maxSteps; step++) {
324
+ for (const sign of [-1, 1]) {
325
+ const lateralOffset = sign * step * LABEL_SEARCH_STEP
326
+ const baseAnchor = {
327
+ x: initialBaseAnchor.x + lateralDir.x * lateralOffset,
328
+ y: initialBaseAnchor.y + lateralDir.y * lateralOffset,
329
+ }
330
+
331
+ const maxSearchDistance = this.getLateralColumnMaxDistance(
332
+ label,
333
+ orientation,
334
+ baseAnchor,
335
+ )
336
+
337
+ const candidate = this.findValidCandidateInShiftColumn({
338
+ label,
339
+ labelIndex,
340
+ orientation,
341
+ direction,
342
+ baseAnchor,
343
+ maxSearchDistance,
344
+ outwardDistance: lateralOffset,
345
+ phase: "lateral-shift",
346
+ })
347
+
348
+ if (candidate) return candidate
349
+ }
350
+ }
351
+
352
+ return null
353
+ }
354
+
269
355
  private findValidCandidateInShiftColumn(params: {
270
356
  label: NetLabelPlacement
271
357
  labelIndex: number
@@ -274,6 +360,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
274
360
  baseAnchor: Point
275
361
  maxSearchDistance: number
276
362
  outwardDistance: number
363
+ phase?: CandidatePhase
277
364
  }) {
278
365
  const {
279
366
  label,
@@ -283,6 +370,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
283
370
  baseAnchor,
284
371
  maxSearchDistance,
285
372
  outwardDistance,
373
+ phase = "shift",
286
374
  } = params
287
375
 
288
376
  for (
@@ -299,7 +387,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
299
387
  candidate,
300
388
  label,
301
389
  labelIndex,
302
- "shift",
390
+ phase,
303
391
  distance,
304
392
  outwardDistance,
305
393
  )
@@ -412,6 +500,32 @@ export class AvailableNetOrientationSolver extends BaseSolver {
412
500
  return Math.min(this.maxSearchDistance, labelLength * 2)
413
501
  }
414
502
 
503
+ private getLateralColumnMaxDistance(
504
+ label: NetLabelPlacement,
505
+ orientation: FacingDirection,
506
+ baseAnchor: Point,
507
+ ) {
508
+ const chipId = label.pinIds
509
+ .map((pid) => this.pinMap[pid]?.chipId)
510
+ .find(Boolean)
511
+ const chip = chipId
512
+ ? this.chipObstacleSpatialIndex.chips.find((c) => c.chipId === chipId)
513
+ : null
514
+
515
+ if (chip) {
516
+ if (orientation === "y-")
517
+ return Math.max(0, baseAnchor.y - chip.bounds.minY)
518
+ if (orientation === "y+")
519
+ return Math.max(0, chip.bounds.maxY - baseAnchor.y)
520
+ if (orientation === "x-")
521
+ return Math.max(0, baseAnchor.x - chip.bounds.minX)
522
+ if (orientation === "x+")
523
+ return Math.max(0, chip.bounds.maxX - baseAnchor.x)
524
+ }
525
+
526
+ return this.getSearchDistanceLimit(label, orientation)
527
+ }
528
+
415
529
  private createCandidate(
416
530
  label: NetLabelPlacement,
417
531
  anchorPoint: Point,
@@ -431,9 +545,25 @@ export class AvailableNetOrientationSolver extends BaseSolver {
431
545
  }
432
546
 
433
547
  private getNetLabelWidth(label: NetLabelPlacement) {
434
- if (!label.netId) return undefined
435
- return this.inputProblem.netConnections.find(
436
- (connection) => connection.netId === label.netId,
548
+ if (label.netId) {
549
+ const ncWidth = this.inputProblem.netConnections.find(
550
+ (connection) => connection.netId === label.netId,
551
+ )?.netLabelWidth
552
+ if (ncWidth !== undefined) return ncWidth
553
+
554
+ const dcWidthByNetId = this.inputProblem.directConnections.find(
555
+ (dc) => dc.netId === label.netId,
556
+ )?.netLabelWidth
557
+ if (dcWidthByNetId !== undefined) return dcWidthByNetId
558
+ }
559
+
560
+ const dcWidthByPinId = this.inputProblem.directConnections.find((dc) =>
561
+ dc.pinIds.some((pid) => label.pinIds.includes(pid)),
562
+ )?.netLabelWidth
563
+ if (dcWidthByPinId !== undefined) return dcWidthByPinId
564
+
565
+ return this.inputProblem.netConnections.find((nc) =>
566
+ nc.pinIds.some((pid) => label.pinIds.includes(pid)),
437
567
  )?.netLabelWidth
438
568
  }
439
569
 
@@ -458,6 +588,12 @@ export class AvailableNetOrientationSolver extends BaseSolver {
458
588
  return "trace-collision"
459
589
  }
460
590
 
591
+ for (const chip of this.chipObstacleSpatialIndex.chips) {
592
+ if (tracePathCrossesAnyBounds(connectorTrace, chip.bounds)) {
593
+ return "chip-collision"
594
+ }
595
+ }
596
+
461
597
  for (let i = 0; i < this.outputNetLabelPlacements.length; i++) {
462
598
  if (i === labelIndex) continue
463
599
  const otherLabel = this.outputNetLabelPlacements[i]!
@@ -168,7 +168,7 @@ export const getConnectorTracePath = (
168
168
  : [source, { x: source.x, y: target.y }, target],
169
169
  )
170
170
 
171
- const simplifyOrthogonalPath = (path: Point[]) => {
171
+ export const simplifyOrthogonalPath = (path: Point[]) => {
172
172
  const deduped = path.filter(
173
173
  (point, index) => index === 0 || !pointsEqual(point, path[index - 1]!),
174
174
  )
@@ -33,7 +33,7 @@ export type CandidateStatus =
33
33
  | "trace-collision"
34
34
  | "netlabel-collision"
35
35
 
36
- export type CandidatePhase = "rotate" | "shift"
36
+ export type CandidatePhase = "rotate" | "shift" | "lateral-shift"
37
37
 
38
38
  export type EvaluatedCandidate = CandidateLabel & {
39
39
  status: CandidateStatus
@@ -1,5 +1,6 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
2
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import { segmentIntersectsRect as segmentIntersectsLabelRect } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
3
4
  import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
4
5
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
6
  import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
@@ -73,9 +74,39 @@ export const generateRerouteCandidateResults = ({
73
74
  outputNetLabelPlacements: NetLabelPlacement[]
74
75
  chipObstacles: ChipObstacle[]
75
76
  }) => {
76
- const rawCandidates = generateCandidatePaths(trace, label, inputProblem)
77
77
  const seen = new Set<string>()
78
78
  const candidateResults: RerouteCandidateResult[] = []
79
+ const horizontalSegmentPushCandidate = generateHorizontalSegmentPushCandidate(
80
+ trace,
81
+ label,
82
+ )
83
+
84
+ if (horizontalSegmentPushCandidate) {
85
+ candidateResults.push(
86
+ createCandidateResult({
87
+ trace,
88
+ obstacleLabel: label,
89
+ path: horizontalSegmentPushCandidate,
90
+ seen,
91
+ outputTraces,
92
+ outputNetLabelPlacements,
93
+ chipObstacles,
94
+ usesHorizontalSegmentPush: true,
95
+ }),
96
+ )
97
+ }
98
+
99
+ const rawCandidates = [
100
+ ...generateLabelHugCandidates(trace, label),
101
+ ...generateRerouteCandidates({
102
+ trace,
103
+ label,
104
+ problem: inputProblem,
105
+ paddingBuffer: LABEL_SIDE_CLEARANCE,
106
+ detourCount: 0,
107
+ }),
108
+ ...generateEndpointDetourCandidates(trace, label),
109
+ ]
79
110
 
80
111
  for (const rawCandidate of rawCandidates) {
81
112
  candidateResults.push(
@@ -94,22 +125,6 @@ export const generateRerouteCandidateResults = ({
94
125
  return candidateResults
95
126
  }
96
127
 
97
- const generateCandidatePaths = (
98
- trace: SolvedTracePath,
99
- label: NetLabelPlacement,
100
- inputProblem: InputProblem,
101
- ) => [
102
- ...generateLabelHugCandidates(trace, label),
103
- ...generateRerouteCandidates({
104
- trace,
105
- label,
106
- problem: inputProblem,
107
- paddingBuffer: LABEL_SIDE_CLEARANCE,
108
- detourCount: 0,
109
- }),
110
- ...generateEndpointDetourCandidates(trace, label),
111
- ]
112
-
113
128
  const createCandidateResult = ({
114
129
  trace,
115
130
  obstacleLabel,
@@ -118,6 +133,7 @@ const createCandidateResult = ({
118
133
  outputTraces,
119
134
  outputNetLabelPlacements,
120
135
  chipObstacles,
136
+ usesHorizontalSegmentPush,
121
137
  }: {
122
138
  trace: SolvedTracePath
123
139
  obstacleLabel: NetLabelPlacement
@@ -126,6 +142,7 @@ const createCandidateResult = ({
126
142
  outputTraces: SolvedTracePath[]
127
143
  outputNetLabelPlacements: NetLabelPlacement[]
128
144
  chipObstacles: ChipObstacle[]
145
+ usesHorizontalSegmentPush?: boolean
129
146
  }): RerouteCandidateResult => {
130
147
  const key = getPathKey(path)
131
148
 
@@ -133,6 +150,7 @@ const createCandidateResult = ({
133
150
  return {
134
151
  path,
135
152
  status: "duplicate",
153
+ usesHorizontalSegmentPush,
136
154
  selected: false,
137
155
  }
138
156
  }
@@ -143,6 +161,7 @@ const createCandidateResult = ({
143
161
  return {
144
162
  path,
145
163
  status: "chip-collision",
164
+ usesHorizontalSegmentPush,
146
165
  selected: false,
147
166
  }
148
167
  }
@@ -157,6 +176,7 @@ const createCandidateResult = ({
157
176
  outputNetLabelPlacements,
158
177
  }),
159
178
  status: "valid",
179
+ usesHorizontalSegmentPush,
160
180
  selected: false,
161
181
  }
162
182
  }
@@ -259,6 +279,13 @@ const selectBestReroutePath = ({
259
279
  outputNetLabelPlacements: NetLabelPlacement[]
260
280
  candidateResults: RerouteCandidateResult[]
261
281
  }) => {
282
+ for (const candidate of candidateResults) {
283
+ if (!candidate.usesHorizontalSegmentPush) continue
284
+ if (candidate.status !== "valid") continue
285
+ if (!candidate.score) continue
286
+ return candidate.path
287
+ }
288
+
262
289
  let bestPath: Point[] | null = null
263
290
  let bestScore = scoreTracePath({
264
291
  trace,
@@ -279,6 +306,99 @@ const selectBestReroutePath = ({
279
306
  return bestPath
280
307
  }
281
308
 
309
+ const generateHorizontalSegmentPushCandidate = (
310
+ trace: SolvedTracePath,
311
+ label: NetLabelPlacement,
312
+ ): Point[] | null => {
313
+ const labelDirection = dir(label.orientation)
314
+ if (labelDirection.x === 0) return null
315
+
316
+ const path = trace.tracePath
317
+ const bounds = getRectBounds(label.center, label.width, label.height)
318
+ const collidingVerticalSegmentIndex = path.findIndex((start, index) => {
319
+ const end = path[index + 1]
320
+ if (!end) return false
321
+ if (Math.abs(start.x - end.x) >= 1e-9) return false
322
+ return segmentIntersectsLabelRect(start, end, bounds)
323
+ })
324
+
325
+ if (
326
+ collidingVerticalSegmentIndex <= 0 ||
327
+ collidingVerticalSegmentIndex >= path.length - 2
328
+ ) {
329
+ return null
330
+ }
331
+
332
+ const previousAnchor = path[collidingVerticalSegmentIndex - 1]!
333
+ const verticalStart = path[collidingVerticalSegmentIndex]!
334
+ const verticalEnd = path[collidingVerticalSegmentIndex + 1]!
335
+ const nextAnchor = path[collidingVerticalSegmentIndex + 2]!
336
+
337
+ if (
338
+ Math.abs(previousAnchor.y - verticalStart.y) >= 1e-9 ||
339
+ Math.abs(verticalEnd.y - nextAnchor.y) >= 1e-9
340
+ ) {
341
+ return null
342
+ }
343
+
344
+ let segmentPushX = bounds.minX - LABEL_SIDE_CLEARANCE
345
+ if (labelDirection.x > 0) {
346
+ segmentPushX = bounds.maxX + LABEL_SIDE_CLEARANCE
347
+ }
348
+ const segmentPushStartY = getClearedHorizontalY({
349
+ start: previousAnchor,
350
+ end: { x: segmentPushX, y: verticalStart.y },
351
+ labelBounds: bounds,
352
+ })
353
+ const segmentPushEndY = getClearedHorizontalY({
354
+ start: { x: segmentPushX, y: verticalEnd.y },
355
+ end: nextAnchor,
356
+ labelBounds: bounds,
357
+ })
358
+
359
+ const segmentPushPath: Point[] = [
360
+ ...path.slice(0, collidingVerticalSegmentIndex - 1),
361
+ previousAnchor,
362
+ ]
363
+
364
+ if (Math.abs(previousAnchor.y - segmentPushStartY) >= 1e-9) {
365
+ segmentPushPath.push({ x: previousAnchor.x, y: segmentPushStartY })
366
+ }
367
+
368
+ segmentPushPath.push({ x: segmentPushX, y: segmentPushStartY })
369
+ segmentPushPath.push({ x: segmentPushX, y: segmentPushEndY })
370
+
371
+ if (Math.abs(nextAnchor.y - segmentPushEndY) >= 1e-9) {
372
+ segmentPushPath.push({ x: nextAnchor.x, y: segmentPushEndY })
373
+ }
374
+
375
+ segmentPushPath.push(
376
+ nextAnchor,
377
+ ...path.slice(collidingVerticalSegmentIndex + 3),
378
+ )
379
+
380
+ return simplifyPath(segmentPushPath)
381
+ }
382
+
383
+ const getClearedHorizontalY = ({
384
+ start,
385
+ end,
386
+ labelBounds,
387
+ }: {
388
+ start: Point
389
+ end: Point
390
+ labelBounds: { minX: number; minY: number; maxX: number; maxY: number }
391
+ }) => {
392
+ if (!segmentIntersectsLabelRect(start, end, labelBounds)) return start.y
393
+
394
+ const labelCenterY = (labelBounds.minY + labelBounds.maxY) / 2
395
+ if (start.y >= labelCenterY) {
396
+ return labelBounds.maxY + LABEL_HUG_CLEARANCE
397
+ }
398
+
399
+ return labelBounds.minY - LABEL_HUG_CLEARANCE
400
+ }
401
+
282
402
  const markSelectedCandidate = (
283
403
  candidateResults: RerouteCandidateResult[],
284
404
  bestPath: Point[] | null,
@@ -32,6 +32,7 @@ export type RerouteCandidateResult = {
32
32
  path: Point[]
33
33
  score?: TracePathScore
34
34
  status: "valid" | "duplicate" | "chip-collision"
35
+ usesHorizontalSegmentPush?: boolean
35
36
  selected: boolean
36
37
  }
37
38
 
@@ -250,6 +250,36 @@ export class NetLabelPlacementSolver extends BaseSolver {
250
250
  return groups
251
251
  }
252
252
 
253
+ private getNetLabelWidthForGroup(
254
+ group: OverlappingSameNetTraceGroup,
255
+ ): number | undefined {
256
+ if (group.netId) {
257
+ const ncWidth = this.inputProblem.netConnections.find(
258
+ (nc) => nc.netId === group.netId,
259
+ )?.netLabelWidth
260
+ if (ncWidth !== undefined) return ncWidth
261
+
262
+ const dcWidthByNetId = this.inputProblem.directConnections.find(
263
+ (dc) => dc.netId === group.netId,
264
+ )?.netLabelWidth
265
+ if (dcWidthByNetId !== undefined) return dcWidthByNetId
266
+ }
267
+
268
+ const pinIds = group.overlappingTraces?.pins.map((p) => p.pinId) ?? []
269
+ if (group.portOnlyPinId) {
270
+ pinIds.push(group.portOnlyPinId)
271
+ }
272
+
273
+ const dcWidthByPinId = this.inputProblem.directConnections.find((dc) =>
274
+ dc.pinIds.some((pid) => pinIds.includes(pid)),
275
+ )?.netLabelWidth
276
+ if (dcWidthByPinId !== undefined) return dcWidthByPinId
277
+
278
+ return this.inputProblem.netConnections.find((nc) =>
279
+ nc.pinIds.some((pid) => pinIds.includes(pid)),
280
+ )?.netLabelWidth
281
+ }
282
+
253
283
  override _step() {
254
284
  if (this.activeSubSolver?.solved) {
255
285
  this.netLabelPlacements.push(this.activeSubSolver.netLabelPlacement!)
@@ -273,11 +303,7 @@ export class NetLabelPlacementSolver extends BaseSolver {
273
303
  this.currentGroup
274
304
  ) {
275
305
  this.triedAnyOrientationFallbackForCurrentGroup = true
276
- const netLabelWidth = this.currentGroup.netId
277
- ? this.inputProblem.netConnections.find(
278
- (nc) => nc.netId === this.currentGroup!.netId,
279
- )?.netLabelWidth
280
- : undefined
306
+ const netLabelWidth = this.getNetLabelWidthForGroup(this.currentGroup)
281
307
  this.activeSubSolver = new SingleNetLabelPlacementSolver({
282
308
  inputProblem: this.inputProblem,
283
309
  inputTraceMap: this.inputTraceMap,
@@ -313,11 +339,7 @@ export class NetLabelPlacementSolver extends BaseSolver {
313
339
  this.currentGroup = nextOverlappingSameNetTraceGroup
314
340
  this.triedAnyOrientationFallbackForCurrentGroup = false
315
341
 
316
- const netLabelWidth = this.currentGroup.netId
317
- ? this.inputProblem.netConnections.find(
318
- (nc) => nc.netId === this.currentGroup!.netId,
319
- )?.netLabelWidth
320
- : undefined
342
+ const netLabelWidth = this.getNetLabelWidthForGroup(this.currentGroup)
321
343
 
322
344
  this.activeSubSolver = new SingleNetLabelPlacementSolver({
323
345
  inputProblem: this.inputProblem,
@@ -338,14 +338,26 @@ const getNetLabelWidth = (
338
338
  inputProblem: InputProblem,
339
339
  label: NetLabelPlacement,
340
340
  ) => {
341
- const configuredWidth = inputProblem.netConnections.find(
341
+ const ncWidthByNetId = inputProblem.netConnections.find(
342
342
  (connection) => connection.netId === label.netId,
343
343
  )?.netLabelWidth
344
- if (configuredWidth !== undefined) return configuredWidth
344
+ if (ncWidthByNetId !== undefined) return ncWidthByNetId
345
345
 
346
- return label.orientation === "y+" || label.orientation === "y-"
347
- ? label.height
348
- : label.width
346
+ const dcWidth = inputProblem.directConnections.find((dc) =>
347
+ dc.pinIds.some((pid) => label.pinIds.includes(pid)),
348
+ )?.netLabelWidth
349
+ if (dcWidth !== undefined) return dcWidth
350
+
351
+ const ncWidthByPinId = inputProblem.netConnections.find((nc) =>
352
+ nc.pinIds.some((pid) => label.pinIds.includes(pid)),
353
+ )?.netLabelWidth
354
+ if (ncWidthByPinId !== undefined) return ncWidthByPinId
355
+
356
+ if (label.orientation === "y+" || label.orientation === "y-") {
357
+ return label.height
358
+ }
359
+
360
+ return label.width
349
361
  }
350
362
 
351
363
  const roundDistance = (distance: number) => Number(distance.toFixed(6))
@@ -1,5 +1,10 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
2
 
3
+ const EPS = 1e-6
4
+
5
+ const sameX = (a: Point, b: Point) => Math.abs(a.x - b.x) <= EPS
6
+ const sameY = (a: Point, b: Point) => Math.abs(a.y - b.y) <= EPS
7
+
3
8
  /**
4
9
  * Checks if a given path of four points forms a rectangle with horizontal and vertical segments.
5
10
  * It verifies if the path forms either an H-V-H "C" shape or a V-H-V "C" shape.
@@ -9,9 +14,9 @@ export const is4PointRectangle = (path: Point[]): boolean => {
9
14
  const [p0, p1, p2, p3] = path
10
15
  // H-V-H "C" shape
11
16
  const isHVHC =
12
- p0.y === p1.y && p1.x === p2.x && p2.y === p3.y && p0.x === p3.x
17
+ sameY(p0, p1) && sameX(p1, p2) && sameY(p2, p3) && sameX(p0, p3)
13
18
  // V-H-V "C" shape
14
19
  const isVHVC =
15
- p0.x === p1.x && p1.y === p2.y && p2.x === p3.x && p0.y === p3.y
20
+ sameX(p0, p1) && sameY(p1, p2) && sameX(p2, p3) && sameY(p0, p3)
16
21
  return isHVHC || isVHVC
17
22
  }
@@ -24,6 +24,7 @@ export interface InputChip {
24
24
  export interface InputDirectConnection {
25
25
  pinIds: [PinId, PinId]
26
26
  netId?: string
27
+ netLabelWidth?: number
27
28
  }
28
29
 
29
30
  export interface InputNetConnection {
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.57",
4
+ "version": "0.0.61",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/example35.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/example36.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/example37.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/example38.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />