@tscircuit/schematic-trace-solver 0.0.121 → 0.0.122

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 (35) hide show
  1. package/dist/index.d.ts +10 -3
  2. package/dist/index.js +196 -28
  3. package/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts +223 -21
  4. package/lib/solvers/RailNetLabelCornerPlacementSolver/types.ts +3 -3
  5. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +34 -9
  6. package/package.json +1 -1
  7. package/site/bug-reports/bug-report-20260804T095800Z.page.tsx +4 -0
  8. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +4 -4
  9. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +5 -5
  10. package/tests/bug-reports/bug-report-20260717T042845Z/__snapshots__/bug-report-20260717T042845Z.snap.svg +12 -12
  11. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
  12. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +2 -2
  13. package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +135 -0
  14. package/tests/bug-reports/bug-report-20260804T095800Z/bug-report-20260804T095800Z.json +634 -0
  15. package/tests/bug-reports/bug-report-20260804T095800Z/bug-report-20260804T095800Z.test.ts +12 -0
  16. package/tests/examples/__snapshots__/example12.snap.svg +2 -2
  17. package/tests/examples/__snapshots__/example13.snap.svg +4 -4
  18. package/tests/examples/__snapshots__/example14.snap.svg +36 -36
  19. package/tests/examples/__snapshots__/example31.snap.svg +6 -6
  20. package/tests/examples/__snapshots__/example43.snap.svg +2 -2
  21. package/tests/examples/__snapshots__/example44.snap.svg +2 -2
  22. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +2 -2
  23. package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +26 -26
  24. package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +2 -2
  25. package/tests/repros/__snapshots__/repro-atmega328p-fault-pullup.snap.svg +2 -2
  26. package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +2 -2
  27. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +2 -2
  28. package/tests/repros/__snapshots__/repro-missing-trace-netlabel.snap.svg +18 -18
  29. package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +3 -3
  30. package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +82 -0
  31. package/tests/repros/assets/repro-nrf52810-clock-routing.input.json +234 -0
  32. package/tests/repros/repro-bq25895-cross-net-trace-overlap.test.ts +13 -0
  33. package/tests/repros/repro-missing-trace-netlabel.test.ts +12 -0
  34. package/tests/repros/repro-netlabel-overlap-trace.test.ts +38 -0
  35. package/tests/repros/repro-nrf52810-clock-routing.test.ts +28 -0
package/dist/index.d.ts CHANGED
@@ -828,11 +828,11 @@ type TraceCornerCandidate = {
828
828
  traceId: string;
829
829
  distance: number;
830
830
  /**
831
- * True when the corner lines up with one of the trace's pins along the
832
- * label's stub axis (same x for a vertical rail label), so the label reads as
833
- * a clean stub off that pin rather than floating over a mid-trace bend.
831
+ * True when the corner is on a vertical rail that connects directly to one
832
+ * of the trace's endpoint pins, rather than on an arbitrary middle bend.
834
833
  */
835
834
  pinAligned: boolean;
835
+ reroutedTracePath?: Point[];
836
836
  };
837
837
  type CornerCandidateStatus = "valid" | "chip-collision" | "text-collision" | "trace-collision" | "netlabel-collision";
838
838
  type EvaluatedCornerCandidate = TraceCornerCandidate & {
@@ -859,6 +859,7 @@ declare class RailNetLabelCornerPlacementSolver extends BaseSolver {
859
859
  getConstructorParams(): ConstructorParameters<typeof RailNetLabelCornerPlacementSolver>[0];
860
860
  _step(): void;
861
861
  getOutput(): {
862
+ traces: SolvedTracePath[];
862
863
  netLabelPlacements: NetLabelPlacement[];
863
864
  };
864
865
  private getProcessableLabelIndices;
@@ -873,6 +874,12 @@ declare class RailNetLabelCornerPlacementSolver extends BaseSolver {
873
874
  private intersectsAnyChip;
874
875
  private intersectsAnyOtherNetLabel;
875
876
  private getCornerCandidatesForLabel;
877
+ private isConfiguredRailLabel;
878
+ private pointsEqual;
879
+ private getVerticalSegmentPointIndices;
880
+ private isPinAlignedCorner;
881
+ private getClearanceShiftedCornerCandidates;
882
+ private isReroutedTraceClear;
876
883
  private getTraceLinesForLabel;
877
884
  private isTraceForLabel;
878
885
  visualize(): GraphicsObject;
package/dist/index.js CHANGED
@@ -9052,6 +9052,7 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
9052
9052
  }
9053
9053
  getOutput() {
9054
9054
  return {
9055
+ traces: this.traces,
9055
9056
  netLabelPlacements: this.outputNetLabelPlacements
9056
9057
  };
9057
9058
  }
@@ -9104,15 +9105,25 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
9104
9105
  center,
9105
9106
  width: label.width,
9106
9107
  height: label.height,
9107
- status: this.getCandidateStatus(bounds, labelIndex),
9108
+ status: this.getCandidateStatus(bounds, labelIndex, candidate),
9108
9109
  selected: false
9109
9110
  };
9110
9111
  }
9111
- getCandidateStatus(bounds, labelIndex) {
9112
+ getCandidateStatus(bounds, labelIndex, candidate) {
9112
9113
  if (this.intersectsAnyChip(bounds)) return "chip-collision";
9113
9114
  if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
9114
9115
  return "text-collision";
9115
- if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
9116
+ const candidateTraceMap = candidate.reroutedTracePath ? {
9117
+ ...this.traceMap,
9118
+ [candidate.traceId]: {
9119
+ ...this.traceMap[candidate.traceId],
9120
+ tracePath: candidate.reroutedTracePath
9121
+ }
9122
+ } : this.traceMap;
9123
+ if (traceCrossesBoundsInterior(bounds, candidateTraceMap)) {
9124
+ return "trace-collision";
9125
+ }
9126
+ if (candidate.reroutedTracePath && !this.isReroutedTraceClear(candidate, labelIndex)) {
9116
9127
  return "trace-collision";
9117
9128
  }
9118
9129
  if (this.intersectsAnyOtherNetLabel(bounds, labelIndex)) {
@@ -9121,6 +9132,23 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
9121
9132
  return "valid";
9122
9133
  }
9123
9134
  applyCandidate(labelIndex, label, candidate) {
9135
+ if (candidate.reroutedTracePath) {
9136
+ const originalTrace = this.traceMap[candidate.traceId];
9137
+ const reroutedTrace = {
9138
+ ...originalTrace,
9139
+ tracePath: candidate.reroutedTracePath
9140
+ };
9141
+ this.traces = this.traces.map(
9142
+ (trace) => trace.mspPairId === candidate.traceId ? reroutedTrace : trace
9143
+ );
9144
+ this.traceMap[candidate.traceId] = reroutedTrace;
9145
+ this.outputNetLabelPlacements = moveAttachedLabelsToReroutedTrace({
9146
+ trace: originalTrace,
9147
+ originalTracePath: originalTrace.tracePath,
9148
+ reroutedTracePath: candidate.reroutedTracePath,
9149
+ netLabelPlacements: this.outputNetLabelPlacements
9150
+ });
9151
+ }
9124
9152
  this.outputNetLabelPlacements[labelIndex] = {
9125
9153
  ...label,
9126
9154
  anchorPoint: candidate.anchorPoint,
@@ -9146,30 +9174,152 @@ var RailNetLabelCornerPlacementSolver = class extends BaseSolver {
9146
9174
  });
9147
9175
  }
9148
9176
  getCornerCandidatesForLabel(label) {
9149
- const isVertical5 = label.orientation === "y+" || label.orientation === "y-";
9150
- const candidates = [];
9151
- const seenCornerKeys = /* @__PURE__ */ new Set();
9152
- for (const trace of this.getTraceLinesForLabel(label)) {
9177
+ const anchorAlignedCandidates = [];
9178
+ const railAlignedCandidates = [];
9179
+ const labelTraces = this.getTraceLinesForLabel(label);
9180
+ const allowRailAlignedFallback = this.isConfiguredRailLabel(label) && !labelTraces.some(
9181
+ (trace) => getTraceCorners(trace.tracePath).some(
9182
+ (corner) => this.pointsEqual(corner, label.anchorPoint)
9183
+ )
9184
+ );
9185
+ for (const trace of labelTraces) {
9153
9186
  const path = trace.tracePath;
9154
9187
  const pins = [path[0], path[path.length - 1]];
9155
9188
  for (const anchorPoint of getTraceCorners(path)) {
9156
- const key = `${anchorPoint.x}:${anchorPoint.y}`;
9157
- if (seenCornerKeys.has(key)) continue;
9158
- seenCornerKeys.add(key);
9159
- const pinAligned = pins.some((pin) => {
9160
- if (isVertical5) return Math.abs(pin.x - anchorPoint.x) < EPS6;
9161
- return Math.abs(pin.y - anchorPoint.y) < EPS6;
9162
- });
9189
+ const cornerIndex = path.indexOf(anchorPoint);
9190
+ const anchorAlignedToPin = pins.some(
9191
+ (pin) => Math.abs(pin.x - anchorPoint.x) <= EPS6
9192
+ );
9193
+ const pinAligned = anchorAlignedToPin || allowRailAlignedFallback && this.isPinAlignedCorner(path, cornerIndex);
9163
9194
  if (!pinAligned) continue;
9164
- candidates.push({
9195
+ const candidates = anchorAlignedToPin ? anchorAlignedCandidates : railAlignedCandidates;
9196
+ candidates.push(
9197
+ {
9198
+ anchorPoint,
9199
+ traceId: trace.mspPairId,
9200
+ distance: getDistance(anchorPoint, label.anchorPoint),
9201
+ pinAligned
9202
+ },
9203
+ ...this.getClearanceShiftedCornerCandidates({
9204
+ label,
9205
+ trace,
9206
+ cornerIndex,
9207
+ pinAligned
9208
+ })
9209
+ );
9210
+ }
9211
+ }
9212
+ return [
9213
+ ...anchorAlignedCandidates.sort((a, b) => a.distance - b.distance),
9214
+ ...railAlignedCandidates.sort((a, b) => a.distance - b.distance)
9215
+ ];
9216
+ }
9217
+ isConfiguredRailLabel(label) {
9218
+ if (!label.netId) return false;
9219
+ return this.inputProblem.availableNetLabelOrientations[label.netId]?.some(
9220
+ (orientation) => orientation === "y+" || orientation === "y-"
9221
+ );
9222
+ }
9223
+ pointsEqual(a, b) {
9224
+ return Math.abs(a.x - b.x) <= EPS6 && Math.abs(a.y - b.y) <= EPS6;
9225
+ }
9226
+ getVerticalSegmentPointIndices(path, cornerIndex) {
9227
+ const corner = path[cornerIndex];
9228
+ const previousIsVertical = Math.abs(path[cornerIndex - 1].x - corner.x) <= EPS6;
9229
+ const nextIsVertical = Math.abs(path[cornerIndex + 1].x - corner.x) <= EPS6;
9230
+ if (previousIsVertical === nextIsVertical) return null;
9231
+ const indices = previousIsVertical ? [cornerIndex - 1, cornerIndex] : [cornerIndex, cornerIndex + 1];
9232
+ return indices;
9233
+ }
9234
+ isPinAlignedCorner(path, cornerIndex) {
9235
+ const verticalPointIndices = this.getVerticalSegmentPointIndices(
9236
+ path,
9237
+ cornerIndex
9238
+ );
9239
+ if (!verticalPointIndices) return false;
9240
+ const [startIndex, endIndex] = verticalPointIndices;
9241
+ return startIndex <= 1 || endIndex >= path.length - 2;
9242
+ }
9243
+ getClearanceShiftedCornerCandidates({
9244
+ label,
9245
+ trace,
9246
+ cornerIndex,
9247
+ pinAligned
9248
+ }) {
9249
+ if (label.orientation !== "y+" && label.orientation !== "y-") return [];
9250
+ const path = trace.tracePath;
9251
+ const corner = path[cornerIndex];
9252
+ const verticalPointIndices = this.getVerticalSegmentPointIndices(
9253
+ path,
9254
+ cornerIndex
9255
+ );
9256
+ if (!verticalPointIndices || verticalPointIndices.includes(0) || verticalPointIndices.includes(path.length - 1)) {
9257
+ return [];
9258
+ }
9259
+ const center = getCenterFromAnchor(
9260
+ corner,
9261
+ label.orientation,
9262
+ label.width,
9263
+ label.height
9264
+ );
9265
+ const labelBounds = getRectBounds(center, label.width, label.height);
9266
+ const collidingChipBounds = this.inputProblem.chips.map((chip) => getRectBounds(chip.center, chip.width, chip.height)).filter((chipBounds) => rectsOverlap(labelBounds, chipBounds));
9267
+ const shiftedCoordinates = /* @__PURE__ */ new Set();
9268
+ for (const chipBounds of collidingChipBounds) {
9269
+ shiftedCoordinates.add(chipBounds.minX - label.width / 2);
9270
+ shiftedCoordinates.add(chipBounds.maxX + label.width / 2);
9271
+ }
9272
+ return [...shiftedCoordinates].flatMap((x) => {
9273
+ if (Math.abs(x - corner.x) <= EPS6) return [];
9274
+ const reroutedTracePath = simplifyPath(
9275
+ path.map(
9276
+ (point, pointIndex) => verticalPointIndices.includes(pointIndex) ? { ...point, x } : point
9277
+ )
9278
+ );
9279
+ const anchorPoint = { x, y: corner.y };
9280
+ const remainsCorner = getTraceCorners(reroutedTracePath).some(
9281
+ (point) => this.pointsEqual(point, anchorPoint)
9282
+ );
9283
+ if (!remainsCorner) return [];
9284
+ return [
9285
+ {
9165
9286
  anchorPoint,
9166
9287
  traceId: trace.mspPairId,
9167
9288
  distance: getDistance(anchorPoint, label.anchorPoint),
9168
- pinAligned
9169
- });
9170
- }
9289
+ pinAligned,
9290
+ reroutedTracePath
9291
+ }
9292
+ ];
9293
+ });
9294
+ }
9295
+ isReroutedTraceClear(candidate, labelIndex) {
9296
+ const reroutedTracePath = candidate.reroutedTracePath;
9297
+ if (isPathCollidingWithObstacles(
9298
+ reroutedTracePath,
9299
+ getObstacleRects(this.inputProblem)
9300
+ )) {
9301
+ return false;
9171
9302
  }
9172
- return candidates.sort((a, b) => a.distance - b.distance);
9303
+ const candidateTrace = this.traceMap[candidate.traceId];
9304
+ const otherNetTraces = this.traces.filter(
9305
+ (trace) => trace.mspPairId !== candidate.traceId && trace.globalConnNetId !== candidateTrace.globalConnNetId
9306
+ );
9307
+ if (doesPathOverlapTraceStrokes(reroutedTracePath, otherNetTraces)) {
9308
+ return false;
9309
+ }
9310
+ const reroutedTraceMap = {
9311
+ [candidate.traceId]: {
9312
+ ...candidateTrace,
9313
+ tracePath: reroutedTracePath
9314
+ }
9315
+ };
9316
+ return this.outputNetLabelPlacements.every((label, index) => {
9317
+ if (index === labelIndex) return true;
9318
+ return !traceCrossesBoundsInterior(
9319
+ getRectBounds(label.center, label.width, label.height),
9320
+ reroutedTraceMap
9321
+ );
9322
+ });
9173
9323
  }
9174
9324
  getTraceLinesForLabel(label) {
9175
9325
  return this.traces.filter(
@@ -11484,9 +11634,7 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
11484
11634
  (instance) => [
11485
11635
  {
11486
11636
  inputProblem: instance.inputProblem,
11487
- traces: Object.values(
11488
- instance.postLabelTraceOverlapShiftSolver.correctedTraceMap
11489
- ),
11637
+ traces: instance.railNetLabelCornerPlacementSolver.getOutput().traces,
11490
11638
  netLabelPlacements: instance.railNetLabelCornerPlacementSolver.outputNetLabelPlacements
11491
11639
  }
11492
11640
  ]
@@ -11497,9 +11645,7 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
11497
11645
  (instance) => [
11498
11646
  {
11499
11647
  inputProblem: instance.inputProblem,
11500
- traces: Object.values(
11501
- instance.postLabelTraceOverlapShiftSolver.correctedTraceMap
11502
- ),
11648
+ traces: instance.railNetLabelCornerPlacementSolver.getOutput().traces,
11503
11649
  netLabelPlacements: instance.traceAnchoredNetLabelOverlapSolver.outputNetLabelPlacements
11504
11650
  }
11505
11651
  ]
@@ -11529,12 +11675,34 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
11529
11675
  "netLabelTraceCollisionSolver",
11530
11676
  NetLabelTraceCollisionSolver,
11531
11677
  (instance) => {
11532
- const previousCollisionOutput = instance.preAlignmentNetLabelTraceCollisionSolver.getOutput();
11678
+ const previousOutput = instance.preAlignmentNetLabelTraceCollisionSolver.getOutput();
11679
+ const alignmentOutput = instance.traceCleanupSolver2.getOutput();
11680
+ const previousTraceMap = new Map(
11681
+ previousOutput.traces.map((trace) => [trace.mspPairId, trace])
11682
+ );
11683
+ let netLabelPlacements = previousOutput.netLabelPlacements;
11684
+ for (const trace of alignmentOutput.traces) {
11685
+ const previousTrace = previousTraceMap.get(trace.mspPairId);
11686
+ if (!previousTrace || previousTrace.tracePath === trace.tracePath) {
11687
+ continue;
11688
+ }
11689
+ const currentLabels = netLabelPlacements;
11690
+ const movedLabels = moveAttachedLabelsToReroutedTrace({
11691
+ trace: previousTrace,
11692
+ originalTracePath: previousTrace.tracePath,
11693
+ reroutedTracePath: trace.tracePath,
11694
+ netLabelPlacements: currentLabels
11695
+ });
11696
+ netLabelPlacements = movedLabels.map((movedLabel, labelIndex) => {
11697
+ const label = currentLabels[labelIndex];
11698
+ return label.orientation === "y+" || label.orientation === "y-" ? movedLabel : label;
11699
+ });
11700
+ }
11533
11701
  return [
11534
11702
  {
11535
11703
  inputProblem: instance.inputProblem,
11536
- traces: instance.traceCleanupSolver2.getOutput().traces,
11537
- netLabelPlacements: previousCollisionOutput.netLabelPlacements
11704
+ traces: alignmentOutput.traces,
11705
+ netLabelPlacements
11538
11706
  }
11539
11707
  ];
11540
11708
  }
@@ -1,13 +1,19 @@
1
1
  import type { GraphicsObject } from "graphics-debug"
2
+ import type { Point } from "@tscircuit/math-utils"
2
3
  import { traceCrossesBoundsInterior } from "lib/solvers/AvailableNetOrientationSolver/geometry"
3
4
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
5
+ import { moveAttachedLabelsToReroutedTrace } from "lib/solvers/Example28Solver/labelMovement"
4
6
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
5
7
  import {
6
8
  getCenterFromAnchor,
7
9
  getRectBounds,
8
10
  } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
9
11
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
12
+ import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
13
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
14
+ import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
10
15
  import type { InputProblem } from "lib/types/InputProblem"
16
+ import { doesPathOverlapTraceStrokes } from "lib/utils/doesPathCoincideWithTraces"
11
17
  import {
12
18
  EPS,
13
19
  getDistance,
@@ -95,6 +101,7 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
95
101
 
96
102
  getOutput() {
97
103
  return {
104
+ traces: this.traces,
98
105
  netLabelPlacements: this.outputNetLabelPlacements,
99
106
  }
100
107
  }
@@ -163,7 +170,7 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
163
170
  center,
164
171
  width: label.width,
165
172
  height: label.height,
166
- status: this.getCandidateStatus(bounds, labelIndex),
173
+ status: this.getCandidateStatus(bounds, labelIndex, candidate),
167
174
  selected: false,
168
175
  }
169
176
  }
@@ -171,11 +178,27 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
171
178
  private getCandidateStatus(
172
179
  bounds: Bounds,
173
180
  labelIndex: number,
181
+ candidate: TraceCornerCandidate,
174
182
  ): CornerCandidateStatus {
175
183
  if (this.intersectsAnyChip(bounds)) return "chip-collision"
176
184
  if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
177
185
  return "text-collision"
178
- if (traceCrossesBoundsInterior(bounds, this.traceMap)) {
186
+ const candidateTraceMap = candidate.reroutedTracePath
187
+ ? {
188
+ ...this.traceMap,
189
+ [candidate.traceId]: {
190
+ ...this.traceMap[candidate.traceId]!,
191
+ tracePath: candidate.reroutedTracePath,
192
+ },
193
+ }
194
+ : this.traceMap
195
+ if (traceCrossesBoundsInterior(bounds, candidateTraceMap)) {
196
+ return "trace-collision"
197
+ }
198
+ if (
199
+ candidate.reroutedTracePath &&
200
+ !this.isReroutedTraceClear(candidate, labelIndex)
201
+ ) {
179
202
  return "trace-collision"
180
203
  }
181
204
  if (this.intersectsAnyOtherNetLabel(bounds, labelIndex)) {
@@ -190,6 +213,24 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
190
213
  label: NetLabelPlacement,
191
214
  candidate: EvaluatedCornerCandidate,
192
215
  ) {
216
+ if (candidate.reroutedTracePath) {
217
+ const originalTrace = this.traceMap[candidate.traceId]!
218
+ const reroutedTrace = {
219
+ ...originalTrace,
220
+ tracePath: candidate.reroutedTracePath,
221
+ }
222
+ this.traces = this.traces.map((trace) =>
223
+ trace.mspPairId === candidate.traceId ? reroutedTrace : trace,
224
+ )
225
+ this.traceMap[candidate.traceId] = reroutedTrace
226
+ this.outputNetLabelPlacements = moveAttachedLabelsToReroutedTrace({
227
+ trace: originalTrace,
228
+ originalTracePath: originalTrace.tracePath,
229
+ reroutedTracePath: candidate.reroutedTracePath,
230
+ netLabelPlacements: this.outputNetLabelPlacements,
231
+ })
232
+ }
233
+
193
234
  this.outputNetLabelPlacements[labelIndex] = {
194
235
  ...label,
195
236
  anchorPoint: candidate.anchorPoint,
@@ -225,36 +266,197 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
225
266
  }
226
267
 
227
268
  private getCornerCandidatesForLabel(label: NetLabelPlacement) {
228
- const isVertical = label.orientation === "y+" || label.orientation === "y-"
229
- const candidates: TraceCornerCandidate[] = []
230
- const seenCornerKeys = new Set<string>()
269
+ const anchorAlignedCandidates: TraceCornerCandidate[] = []
270
+ const railAlignedCandidates: TraceCornerCandidate[] = []
271
+ const labelTraces = this.getTraceLinesForLabel(label)
272
+ const allowRailAlignedFallback =
273
+ this.isConfiguredRailLabel(label) &&
274
+ !labelTraces.some((trace) =>
275
+ getTraceCorners(trace.tracePath).some((corner) =>
276
+ this.pointsEqual(corner, label.anchorPoint),
277
+ ),
278
+ )
231
279
 
232
- for (const trace of this.getTraceLinesForLabel(label)) {
280
+ for (const trace of labelTraces) {
233
281
  const path = trace.tracePath
234
282
  const pins = [path[0]!, path[path.length - 1]!]
235
283
  for (const anchorPoint of getTraceCorners(path)) {
236
- const key = `${anchorPoint.x}:${anchorPoint.y}`
237
- if (seenCornerKeys.has(key)) continue
238
- seenCornerKeys.add(key)
239
- const pinAligned = pins.some((pin) => {
240
- if (isVertical) return Math.abs(pin.x - anchorPoint.x) < EPS
241
- return Math.abs(pin.y - anchorPoint.y) < EPS
242
- })
243
- // Only pin-aligned corners are worth snapping a rail label to (a clean
244
- // stub off a pin). Non-aligned corners are no better than where the
245
- // label already is, so they aren't offered as candidates — this keeps
246
- // already-well-placed labels untouched.
284
+ const cornerIndex = path.indexOf(anchorPoint)
285
+ const anchorAlignedToPin = pins.some(
286
+ (pin) => Math.abs(pin.x - anchorPoint.x) <= EPS,
287
+ )
288
+ const pinAligned =
289
+ anchorAlignedToPin ||
290
+ (allowRailAlignedFallback &&
291
+ this.isPinAlignedCorner(path, cornerIndex))
247
292
  if (!pinAligned) continue
248
- candidates.push({
293
+
294
+ const candidates = anchorAlignedToPin
295
+ ? anchorAlignedCandidates
296
+ : railAlignedCandidates
297
+ candidates.push(
298
+ {
299
+ anchorPoint,
300
+ traceId: trace.mspPairId,
301
+ distance: getDistance(anchorPoint, label.anchorPoint),
302
+ pinAligned,
303
+ },
304
+ ...this.getClearanceShiftedCornerCandidates({
305
+ label,
306
+ trace,
307
+ cornerIndex,
308
+ pinAligned,
309
+ }),
310
+ )
311
+ }
312
+ }
313
+
314
+ return [
315
+ ...anchorAlignedCandidates.sort((a, b) => a.distance - b.distance),
316
+ ...railAlignedCandidates.sort((a, b) => a.distance - b.distance),
317
+ ]
318
+ }
319
+
320
+ private isConfiguredRailLabel(label: NetLabelPlacement) {
321
+ if (!label.netId) return false
322
+ return this.inputProblem.availableNetLabelOrientations[label.netId]?.some(
323
+ (orientation) => orientation === "y+" || orientation === "y-",
324
+ )
325
+ }
326
+
327
+ private pointsEqual(a: Point, b: Point) {
328
+ return Math.abs(a.x - b.x) <= EPS && Math.abs(a.y - b.y) <= EPS
329
+ }
330
+
331
+ private getVerticalSegmentPointIndices(path: Point[], cornerIndex: number) {
332
+ const corner = path[cornerIndex]!
333
+ const previousIsVertical =
334
+ Math.abs(path[cornerIndex - 1]!.x - corner.x) <= EPS
335
+ const nextIsVertical = Math.abs(path[cornerIndex + 1]!.x - corner.x) <= EPS
336
+ if (previousIsVertical === nextIsVertical) return null
337
+
338
+ const indices = previousIsVertical
339
+ ? [cornerIndex - 1, cornerIndex]
340
+ : [cornerIndex, cornerIndex + 1]
341
+ return indices
342
+ }
343
+
344
+ private isPinAlignedCorner(path: Point[], cornerIndex: number) {
345
+ const verticalPointIndices = this.getVerticalSegmentPointIndices(
346
+ path,
347
+ cornerIndex,
348
+ )
349
+ if (!verticalPointIndices) return false
350
+
351
+ const [startIndex, endIndex] = verticalPointIndices
352
+ return startIndex! <= 1 || endIndex! >= path.length - 2
353
+ }
354
+
355
+ private getClearanceShiftedCornerCandidates({
356
+ label,
357
+ trace,
358
+ cornerIndex,
359
+ pinAligned,
360
+ }: {
361
+ label: NetLabelPlacement
362
+ trace: SolvedTracePath
363
+ cornerIndex: number
364
+ pinAligned: boolean
365
+ }): TraceCornerCandidate[] {
366
+ if (label.orientation !== "y+" && label.orientation !== "y-") return []
367
+
368
+ const path = trace.tracePath
369
+ const corner = path[cornerIndex]!
370
+ const verticalPointIndices = this.getVerticalSegmentPointIndices(
371
+ path,
372
+ cornerIndex,
373
+ )
374
+ if (
375
+ !verticalPointIndices ||
376
+ verticalPointIndices.includes(0) ||
377
+ verticalPointIndices.includes(path.length - 1)
378
+ ) {
379
+ return []
380
+ }
381
+
382
+ const center = getCenterFromAnchor(
383
+ corner,
384
+ label.orientation,
385
+ label.width,
386
+ label.height,
387
+ )
388
+ const labelBounds = getRectBounds(center, label.width, label.height)
389
+ const collidingChipBounds = this.inputProblem.chips
390
+ .map((chip) => getRectBounds(chip.center, chip.width, chip.height))
391
+ .filter((chipBounds) => rectsOverlap(labelBounds, chipBounds))
392
+ const shiftedCoordinates = new Set<number>()
393
+ for (const chipBounds of collidingChipBounds) {
394
+ shiftedCoordinates.add(chipBounds.minX - label.width / 2)
395
+ shiftedCoordinates.add(chipBounds.maxX + label.width / 2)
396
+ }
397
+
398
+ return [...shiftedCoordinates].flatMap((x) => {
399
+ if (Math.abs(x - corner.x) <= EPS) return []
400
+ const reroutedTracePath = simplifyPath(
401
+ path.map((point, pointIndex) =>
402
+ verticalPointIndices.includes(pointIndex) ? { ...point, x } : point,
403
+ ),
404
+ )
405
+ const anchorPoint = { x, y: corner.y }
406
+ const remainsCorner = getTraceCorners(reroutedTracePath).some((point) =>
407
+ this.pointsEqual(point, anchorPoint),
408
+ )
409
+ if (!remainsCorner) return []
410
+
411
+ return [
412
+ {
249
413
  anchorPoint,
250
414
  traceId: trace.mspPairId,
251
415
  distance: getDistance(anchorPoint, label.anchorPoint),
252
416
  pinAligned,
253
- })
254
- }
417
+ reroutedTracePath,
418
+ },
419
+ ]
420
+ })
421
+ }
422
+
423
+ private isReroutedTraceClear(
424
+ candidate: TraceCornerCandidate,
425
+ labelIndex: number,
426
+ ) {
427
+ const reroutedTracePath = candidate.reroutedTracePath!
428
+ if (
429
+ isPathCollidingWithObstacles(
430
+ reroutedTracePath,
431
+ getObstacleRects(this.inputProblem),
432
+ )
433
+ ) {
434
+ return false
435
+ }
436
+
437
+ const candidateTrace = this.traceMap[candidate.traceId]!
438
+ const otherNetTraces = this.traces.filter(
439
+ (trace) =>
440
+ trace.mspPairId !== candidate.traceId &&
441
+ trace.globalConnNetId !== candidateTrace.globalConnNetId,
442
+ )
443
+ if (doesPathOverlapTraceStrokes(reroutedTracePath, otherNetTraces)) {
444
+ return false
255
445
  }
256
446
 
257
- return candidates.sort((a, b) => a.distance - b.distance)
447
+ const reroutedTraceMap = {
448
+ [candidate.traceId]: {
449
+ ...candidateTrace,
450
+ tracePath: reroutedTracePath,
451
+ },
452
+ }
453
+ return this.outputNetLabelPlacements.every((label, index) => {
454
+ if (index === labelIndex) return true
455
+ return !traceCrossesBoundsInterior(
456
+ getRectBounds(label.center, label.width, label.height),
457
+ reroutedTraceMap,
458
+ )
459
+ })
258
460
  }
259
461
 
260
462
  private getTraceLinesForLabel(label: NetLabelPlacement) {
@@ -21,11 +21,11 @@ export type TraceCornerCandidate = {
21
21
  traceId: string
22
22
  distance: number
23
23
  /**
24
- * True when the corner lines up with one of the trace's pins along the
25
- * label's stub axis (same x for a vertical rail label), so the label reads as
26
- * a clean stub off that pin rather than floating over a mid-trace bend.
24
+ * True when the corner is on a vertical rail that connects directly to one
25
+ * of the trace's endpoint pins, rather than on an arbitrary middle bend.
27
26
  */
28
27
  pinAligned: boolean
28
+ reroutedTracePath?: Point[]
29
29
  }
30
30
 
31
31
  export type CornerCandidateStatus =