@tscircuit/schematic-trace-solver 0.0.82 → 0.0.83

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/biome.json CHANGED
@@ -19,6 +19,11 @@
19
19
  "bracketSameLine": false
20
20
  }
21
21
  },
22
+ "json": {
23
+ "formatter": {
24
+ "enabled": false
25
+ }
26
+ },
22
27
  "linter": {
23
28
  "enabled": true,
24
29
  "rules": {
package/dist/index.d.ts CHANGED
@@ -666,6 +666,7 @@ declare class Example28Solver extends BaseSolver {
666
666
  currentOverlap: TraceLabelOverlap | null;
667
667
  currentCandidateResults: RerouteCandidateResult[];
668
668
  private chipObstacles;
669
+ private pinMap;
669
670
  constructor(params: Example28SolverParams);
670
671
  getConstructorParams(): ConstructorParameters<typeof Example28Solver>[0];
671
672
  _step(): void;
@@ -679,6 +680,7 @@ declare class Example28Solver extends BaseSolver {
679
680
  private isOverlapStillPresent;
680
681
  private getCurrentTrace;
681
682
  private rerouteOverlappingTrace;
683
+ private tryMoveLabelOutward;
682
684
  visualize(): GraphicsObject;
683
685
  }
684
686
 
package/dist/index.js CHANGED
@@ -5162,6 +5162,54 @@ var TraceCleanupSolver = class extends BaseSolver {
5162
5162
  }
5163
5163
  };
5164
5164
 
5165
+ // lib/solvers/AvailableNetOrientationSolver/traces.ts
5166
+ var getPinMap = (inputProblem) => {
5167
+ const pinMap = {};
5168
+ for (const chip of inputProblem.chips) {
5169
+ for (const pin of chip.pins) {
5170
+ pinMap[pin.pinId] = { ...pin, chipId: chip.chipId };
5171
+ }
5172
+ }
5173
+ return pinMap;
5174
+ };
5175
+ var getTracePins = (label, pinMap) => {
5176
+ const pins = label.pinIds.flatMap((pinId) => {
5177
+ const pin = pinMap[pinId];
5178
+ return pin ? [pin] : [];
5179
+ });
5180
+ if (pins.length >= 2) return [pins[0], pins[1]];
5181
+ if (pins.length === 1) return [pins[0], pins[0]];
5182
+ const syntheticPin = {
5183
+ pinId: `${label.globalConnNetId}-netlabel-anchor`,
5184
+ x: label.anchorPoint.x,
5185
+ y: label.anchorPoint.y,
5186
+ chipId: "available-net-orientation"
5187
+ };
5188
+ return [syntheticPin, syntheticPin];
5189
+ };
5190
+ var toNetLabelPlacementPatch = (candidate) => ({
5191
+ orientation: candidate.orientation,
5192
+ anchorPoint: candidate.anchorPoint,
5193
+ center: candidate.center,
5194
+ width: candidate.width,
5195
+ height: candidate.height
5196
+ });
5197
+
5198
+ // lib/utils/dir.ts
5199
+ var dir = (facingDirection) => {
5200
+ switch (facingDirection) {
5201
+ case "x+":
5202
+ return { x: 1, y: 0 };
5203
+ case "x-":
5204
+ return { x: -1, y: 0 };
5205
+ case "y+":
5206
+ return { x: 0, y: 1 };
5207
+ case "y-":
5208
+ return { x: 0, y: -1 };
5209
+ }
5210
+ throw new Error(`Invalid facing direction: ${facingDirection}`);
5211
+ };
5212
+
5165
5213
  // lib/solvers/Example28Solver/types.ts
5166
5214
  var EPS7 = 1e-9;
5167
5215
 
@@ -5369,24 +5417,8 @@ var moveAttachedLabelsToReroutedTrace = ({
5369
5417
  };
5370
5418
  });
5371
5419
 
5372
- // lib/utils/dir.ts
5373
- var dir = (facingDirection) => {
5374
- switch (facingDirection) {
5375
- case "x+":
5376
- return { x: 1, y: 0 };
5377
- case "x-":
5378
- return { x: -1, y: 0 };
5379
- case "y+":
5380
- return { x: 0, y: 1 };
5381
- case "y-":
5382
- return { x: 0, y: -1 };
5383
- }
5384
- throw new Error(`Invalid facing direction: ${facingDirection}`);
5385
- };
5386
-
5387
5420
  // lib/solvers/Example28Solver/reroute.ts
5388
- var LABEL_SIDE_CLEARANCE = 0.1;
5389
- var LABEL_HUG_CLEARANCE = 1e-3;
5421
+ var LABEL_CLEARANCE = 0.1;
5390
5422
  var findBestReroutePath = ({
5391
5423
  trace,
5392
5424
  obstacleLabel,
@@ -5447,7 +5479,7 @@ var generateRerouteCandidateResults = ({
5447
5479
  trace,
5448
5480
  label,
5449
5481
  problem: inputProblem,
5450
- paddingBuffer: LABEL_SIDE_CLEARANCE,
5482
+ paddingBuffer: LABEL_CLEARANCE,
5451
5483
  detourCount: 0
5452
5484
  }),
5453
5485
  ...generateEndpointDetourCandidates(trace, label)
@@ -5513,8 +5545,10 @@ var generateEndpointDetourCandidates = (trace, label) => {
5513
5545
  const start = trace.tracePath[0];
5514
5546
  const end = trace.tracePath[trace.tracePath.length - 1];
5515
5547
  if (!start || !end) return [];
5548
+ const startExit = trace.tracePath[1] ?? start;
5549
+ const endEntry = trace.tracePath[trace.tracePath.length - 2] ?? end;
5516
5550
  const bounds = getRectBounds(label.center, label.width, label.height);
5517
- const padding = LABEL_SIDE_CLEARANCE;
5551
+ const padding = LABEL_CLEARANCE;
5518
5552
  const labelDirection = dir(label.orientation);
5519
5553
  const labelSideX = labelDirection.x < 0 ? bounds.minX - padding : bounds.maxX + padding;
5520
5554
  const oppositeSideX = labelDirection.x < 0 ? bounds.maxX + padding : bounds.minX - padding;
@@ -5525,18 +5559,22 @@ var generateEndpointDetourCandidates = (trace, label) => {
5525
5559
  candidates.push(
5526
5560
  [
5527
5561
  start,
5528
- { x: start.x, y: topY },
5562
+ startExit,
5563
+ { x: startExit.x, y: topY },
5529
5564
  { x: sideX, y: topY },
5530
5565
  { x: sideX, y: bottomY },
5531
- { x: end.x, y: bottomY },
5566
+ { x: endEntry.x, y: bottomY },
5567
+ endEntry,
5532
5568
  end
5533
5569
  ],
5534
5570
  [
5535
5571
  start,
5536
- { x: start.x, y: bottomY },
5572
+ startExit,
5573
+ { x: startExit.x, y: bottomY },
5537
5574
  { x: sideX, y: bottomY },
5538
5575
  { x: sideX, y: topY },
5539
- { x: end.x, y: topY },
5576
+ { x: endEntry.x, y: topY },
5577
+ endEntry,
5540
5578
  end
5541
5579
  ]
5542
5580
  );
@@ -5547,30 +5585,36 @@ var generateLabelHugCandidates = (trace, label) => {
5547
5585
  const start = trace.tracePath[0];
5548
5586
  const end = trace.tracePath[trace.tracePath.length - 1];
5549
5587
  if (!start || !end) return [];
5588
+ const startExit = trace.tracePath[1] ?? start;
5589
+ const endEntry = trace.tracePath[trace.tracePath.length - 2] ?? end;
5550
5590
  const labelDirection = dir(label.orientation);
5551
5591
  if (labelDirection.x === 0) return [];
5552
5592
  const bounds = getRectBounds(label.center, label.width, label.height);
5553
- const sideX = labelDirection.x < 0 ? bounds.minX - LABEL_SIDE_CLEARANCE : bounds.maxX + LABEL_SIDE_CLEARANCE;
5554
- const topY = bounds.maxY + LABEL_HUG_CLEARANCE;
5555
- const bottomY = bounds.minY - LABEL_HUG_CLEARANCE;
5556
- const startsAboveEnd = start.y >= end.y;
5593
+ const sideX = labelDirection.x < 0 ? bounds.minX - LABEL_CLEARANCE : bounds.maxX + LABEL_CLEARANCE;
5594
+ const topY = bounds.maxY + LABEL_CLEARANCE;
5595
+ const bottomY = bounds.minY - LABEL_CLEARANCE;
5596
+ const startsAboveEnd = startExit.y >= endEntry.y;
5557
5597
  const firstY = startsAboveEnd ? topY : bottomY;
5558
5598
  const secondY = startsAboveEnd ? bottomY : topY;
5559
5599
  return [
5560
5600
  [
5561
5601
  start,
5562
- { x: start.x, y: firstY },
5602
+ startExit,
5603
+ { x: startExit.x, y: firstY },
5563
5604
  { x: sideX, y: firstY },
5564
5605
  { x: sideX, y: secondY },
5565
- { x: end.x, y: secondY },
5606
+ { x: endEntry.x, y: secondY },
5607
+ endEntry,
5566
5608
  end
5567
5609
  ],
5568
5610
  [
5569
5611
  start,
5570
- { x: start.x, y: secondY },
5612
+ startExit,
5613
+ { x: startExit.x, y: secondY },
5571
5614
  { x: sideX, y: secondY },
5572
5615
  { x: sideX, y: firstY },
5573
- { x: end.x, y: firstY },
5616
+ { x: endEntry.x, y: firstY },
5617
+ endEntry,
5574
5618
  end
5575
5619
  ]
5576
5620
  ];
@@ -5625,9 +5669,9 @@ var generateHorizontalSegmentPushCandidate = (trace, label) => {
5625
5669
  if (Math.abs(previousAnchor.y - verticalStart.y) >= 1e-9 || Math.abs(verticalEnd.y - nextAnchor.y) >= 1e-9) {
5626
5670
  return null;
5627
5671
  }
5628
- let segmentPushX = bounds.minX - LABEL_SIDE_CLEARANCE;
5672
+ let segmentPushX = bounds.minX - LABEL_CLEARANCE;
5629
5673
  if (labelDirection.x > 0) {
5630
- segmentPushX = bounds.maxX + LABEL_SIDE_CLEARANCE;
5674
+ segmentPushX = bounds.maxX + LABEL_CLEARANCE;
5631
5675
  }
5632
5676
  const segmentPushStartY = getClearedHorizontalY({
5633
5677
  start: previousAnchor,
@@ -5665,9 +5709,9 @@ var getClearedHorizontalY = ({
5665
5709
  if (!segmentIntersectsRect2(start, end, labelBounds)) return start.y;
5666
5710
  const labelCenterY = (labelBounds.minY + labelBounds.maxY) / 2;
5667
5711
  if (start.y >= labelCenterY) {
5668
- return labelBounds.maxY + LABEL_HUG_CLEARANCE;
5712
+ return labelBounds.maxY + LABEL_CLEARANCE;
5669
5713
  }
5670
- return labelBounds.minY - LABEL_HUG_CLEARANCE;
5714
+ return labelBounds.minY - LABEL_CLEARANCE;
5671
5715
  };
5672
5716
  var markSelectedCandidate = (candidateResults, bestPath) => {
5673
5717
  if (!bestPath) return;
@@ -5800,6 +5844,9 @@ ${label.netId ?? label.globalConnNetId}`
5800
5844
  };
5801
5845
 
5802
5846
  // lib/solvers/Example28Solver/Example28Solver.ts
5847
+ var LABEL_OUTWARD_STEP = 0.1;
5848
+ var LABEL_MAX_OUTWARD_STEPS = 10;
5849
+ var LABEL_TRACE_CLEARANCE = 0.1;
5803
5850
  var Example28Solver = class extends BaseSolver {
5804
5851
  inputProblem;
5805
5852
  traces;
@@ -5810,6 +5857,7 @@ var Example28Solver = class extends BaseSolver {
5810
5857
  currentOverlap = null;
5811
5858
  currentCandidateResults = [];
5812
5859
  chipObstacles;
5860
+ pinMap;
5813
5861
  constructor(params) {
5814
5862
  super();
5815
5863
  this.inputProblem = params.inputProblem;
@@ -5818,6 +5866,7 @@ var Example28Solver = class extends BaseSolver {
5818
5866
  this.outputTraces = [...params.traces];
5819
5867
  this.outputNetLabelPlacements = [...params.netLabelPlacements];
5820
5868
  this.chipObstacles = getObstacleRects(params.inputProblem);
5869
+ this.pinMap = getPinMap(params.inputProblem);
5821
5870
  this.initializeQueuedOverlaps();
5822
5871
  this.currentOverlap = this.queuedOverlaps[0] ?? null;
5823
5872
  }
@@ -5900,6 +5949,7 @@ var Example28Solver = class extends BaseSolver {
5900
5949
  );
5901
5950
  if (traceIndex === -1) return;
5902
5951
  const currentTrace = this.outputTraces[traceIndex];
5952
+ if (this.tryMoveLabelOutward(overlap.label)) return;
5903
5953
  const rerouteResult = findBestReroutePath({
5904
5954
  trace: currentTrace,
5905
5955
  obstacleLabel: overlap.label,
@@ -5922,6 +5972,62 @@ var Example28Solver = class extends BaseSolver {
5922
5972
  netLabelPlacements: this.outputNetLabelPlacements
5923
5973
  });
5924
5974
  }
5975
+ tryMoveLabelOutward(labelToMove) {
5976
+ const labelIndex = this.outputNetLabelPlacements.findIndex(
5977
+ (label2) => label2.globalConnNetId === labelToMove.globalConnNetId && label2.anchorPoint.x === labelToMove.anchorPoint.x && label2.anchorPoint.y === labelToMove.anchorPoint.y && label2.pinIds.join(",") === labelToMove.pinIds.join(",")
5978
+ );
5979
+ if (labelIndex === -1) return false;
5980
+ const label = this.outputNetLabelPlacements[labelIndex];
5981
+ if (label.mspConnectionPairIds.length > 0) return false;
5982
+ if (label.pinIds.length !== 1) return false;
5983
+ if (this.outputNetLabelPlacements.filter(
5984
+ (otherLabel) => otherLabel.globalConnNetId === label.globalConnNetId
5985
+ ).length !== 1) {
5986
+ return false;
5987
+ }
5988
+ const outward = dir(label.orientation);
5989
+ if (outward.x === 0 && outward.y === 0) return false;
5990
+ for (let step = 1; step <= LABEL_MAX_OUTWARD_STEPS; step++) {
5991
+ const distance3 = step * LABEL_OUTWARD_STEP;
5992
+ const candidate = {
5993
+ ...label,
5994
+ anchorPoint: {
5995
+ x: label.anchorPoint.x + outward.x * distance3,
5996
+ y: label.anchorPoint.y + outward.y * distance3
5997
+ },
5998
+ center: {
5999
+ x: label.center.x + outward.x * distance3,
6000
+ y: label.center.y + outward.y * distance3
6001
+ }
6002
+ };
6003
+ const candidateWithClearance = {
6004
+ ...candidate,
6005
+ width: candidate.width + LABEL_TRACE_CLEARANCE * 2,
6006
+ height: candidate.height + LABEL_TRACE_CLEARANCE * 2
6007
+ };
6008
+ if (detectTraceLabelOverlap({
6009
+ traces: this.outputTraces,
6010
+ netLabels: [candidateWithClearance]
6011
+ }).length > 0) {
6012
+ continue;
6013
+ }
6014
+ const connectorTrace = createPortOnlyLabelConnectorTrace({
6015
+ label,
6016
+ movedLabel: candidate,
6017
+ pinMap: this.pinMap
6018
+ });
6019
+ if (detectTraceLabelOverlap({
6020
+ traces: [connectorTrace],
6021
+ netLabels: this.outputNetLabelPlacements
6022
+ }).length > 0) {
6023
+ continue;
6024
+ }
6025
+ this.outputNetLabelPlacements[labelIndex] = candidate;
6026
+ this.outputTraces.push(connectorTrace);
6027
+ return true;
6028
+ }
6029
+ return false;
6030
+ }
5925
6031
  visualize() {
5926
6032
  return visualizeExample28Solver({
5927
6033
  inputProblem: this.inputProblem,
@@ -5933,6 +6039,24 @@ var Example28Solver = class extends BaseSolver {
5933
6039
  });
5934
6040
  }
5935
6041
  };
6042
+ var getPortOnlyLabelConnectorMspPairId = (label) => `port-only-label-connector-${label.globalConnNetId}-${label.pinIds[0]}`;
6043
+ var createPortOnlyLabelConnectorTrace = ({
6044
+ label,
6045
+ movedLabel,
6046
+ pinMap
6047
+ }) => {
6048
+ const mspPairId = getPortOnlyLabelConnectorMspPairId(label);
6049
+ return {
6050
+ mspPairId,
6051
+ dcConnNetId: label.dcConnNetId ?? label.globalConnNetId,
6052
+ globalConnNetId: label.globalConnNetId,
6053
+ userNetId: label.netId,
6054
+ pins: getTracePins(label, pinMap),
6055
+ tracePath: [label.anchorPoint, movedLabel.anchorPoint],
6056
+ mspConnectionPairIds: [mspPairId],
6057
+ pinIds: label.pinIds
6058
+ };
6059
+ };
5936
6060
 
5937
6061
  // lib/solvers/AvailableNetOrientationSolver/constants.ts
5938
6062
  var LABEL_SEARCH_STEP = 0.05;
@@ -6046,39 +6170,6 @@ var getMaxSearchDistance = (inputProblem) => {
6046
6170
  return maxChipWidth * 3;
6047
6171
  };
6048
6172
 
6049
- // lib/solvers/AvailableNetOrientationSolver/traces.ts
6050
- var getPinMap = (inputProblem) => {
6051
- const pinMap = {};
6052
- for (const chip of inputProblem.chips) {
6053
- for (const pin of chip.pins) {
6054
- pinMap[pin.pinId] = { ...pin, chipId: chip.chipId };
6055
- }
6056
- }
6057
- return pinMap;
6058
- };
6059
- var getTracePins = (label, pinMap) => {
6060
- const pins = label.pinIds.flatMap((pinId) => {
6061
- const pin = pinMap[pinId];
6062
- return pin ? [pin] : [];
6063
- });
6064
- if (pins.length >= 2) return [pins[0], pins[1]];
6065
- if (pins.length === 1) return [pins[0], pins[0]];
6066
- const syntheticPin = {
6067
- pinId: `${label.globalConnNetId}-netlabel-anchor`,
6068
- x: label.anchorPoint.x,
6069
- y: label.anchorPoint.y,
6070
- chipId: "available-net-orientation"
6071
- };
6072
- return [syntheticPin, syntheticPin];
6073
- };
6074
- var toNetLabelPlacementPatch = (candidate) => ({
6075
- orientation: candidate.orientation,
6076
- anchorPoint: candidate.anchorPoint,
6077
- center: candidate.center,
6078
- width: candidate.width,
6079
- height: candidate.height
6080
- });
6081
-
6082
6173
  // lib/solvers/AvailableNetOrientationSolver/visualize.ts
6083
6174
  var visualizeAvailableNetOrientationSolver = (params) => {
6084
6175
  const graphics = visualizeInputProblem(params.inputProblem);
@@ -6167,7 +6258,7 @@ var ensureGraphicsArrays = (graphics) => {
6167
6258
  };
6168
6259
 
6169
6260
  // lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts
6170
- var LABEL_TRACE_CLEARANCE = 0.1;
6261
+ var LABEL_TRACE_CLEARANCE2 = 0.1;
6171
6262
  var AvailableNetOrientationSolver = class extends BaseSolver {
6172
6263
  inputProblem;
6173
6264
  traces;
@@ -6725,10 +6816,10 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
6725
6816
  }
6726
6817
  getLabelTraceClearanceBounds(bounds) {
6727
6818
  return {
6728
- minX: bounds.minX - LABEL_TRACE_CLEARANCE,
6729
- minY: bounds.minY - LABEL_TRACE_CLEARANCE,
6730
- maxX: bounds.maxX + LABEL_TRACE_CLEARANCE,
6731
- maxY: bounds.maxY + LABEL_TRACE_CLEARANCE
6819
+ minX: bounds.minX - LABEL_TRACE_CLEARANCE2,
6820
+ minY: bounds.minY - LABEL_TRACE_CLEARANCE2,
6821
+ maxX: bounds.maxX + LABEL_TRACE_CLEARANCE2,
6822
+ maxY: bounds.maxY + LABEL_TRACE_CLEARANCE2
6732
6823
  };
6733
6824
  }
6734
6825
  shouldCheckTraceClearanceForLabel(label) {
@@ -1,18 +1,27 @@
1
1
  import type { GraphicsObject } from "graphics-debug"
2
+ import {
3
+ getPinMap,
4
+ getTracePins,
5
+ } from "lib/solvers/AvailableNetOrientationSolver/traces"
2
6
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
3
7
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
- import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
8
  import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
9
+ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
6
10
  import {
7
11
  detectTraceLabelOverlap,
8
12
  type TraceLabelOverlap,
9
13
  } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
10
- import type { InputProblem } from "lib/types/InputProblem"
14
+ import type { InputPin, InputProblem } from "lib/types/InputProblem"
15
+ import { dir } from "lib/utils/dir"
11
16
  import { moveAttachedLabelsToReroutedTrace } from "./labelMovement"
12
17
  import { findBestReroutePath } from "./reroute"
13
18
  import type { Example28SolverParams, RerouteCandidateResult } from "./types"
14
19
  import { visualizeExample28Solver } from "./visualize"
15
20
 
21
+ const LABEL_OUTWARD_STEP = 0.1
22
+ const LABEL_MAX_OUTWARD_STEPS = 10
23
+ const LABEL_TRACE_CLEARANCE = 0.1
24
+
16
25
  export class Example28Solver extends BaseSolver {
17
26
  inputProblem: InputProblem
18
27
  traces: SolvedTracePath[]
@@ -25,6 +34,7 @@ export class Example28Solver extends BaseSolver {
25
34
  currentCandidateResults: RerouteCandidateResult[] = []
26
35
 
27
36
  private chipObstacles: ReturnType<typeof getObstacleRects>
37
+ private pinMap: Record<string, InputPin & { chipId: string }>
28
38
 
29
39
  constructor(params: Example28SolverParams) {
30
40
  super()
@@ -34,6 +44,7 @@ export class Example28Solver extends BaseSolver {
34
44
  this.outputTraces = [...params.traces]
35
45
  this.outputNetLabelPlacements = [...params.netLabelPlacements]
36
46
  this.chipObstacles = getObstacleRects(params.inputProblem)
47
+ this.pinMap = getPinMap(params.inputProblem)
37
48
  this.initializeQueuedOverlaps()
38
49
  this.currentOverlap = this.queuedOverlaps[0] ?? null
39
50
  }
@@ -144,6 +155,8 @@ export class Example28Solver extends BaseSolver {
144
155
  if (traceIndex === -1) return
145
156
 
146
157
  const currentTrace = this.outputTraces[traceIndex]!
158
+ if (this.tryMoveLabelOutward(overlap.label)) return
159
+
147
160
  const rerouteResult = findBestReroutePath({
148
161
  trace: currentTrace,
149
162
  obstacleLabel: overlap.label,
@@ -169,6 +182,79 @@ export class Example28Solver extends BaseSolver {
169
182
  })
170
183
  }
171
184
 
185
+ private tryMoveLabelOutward(labelToMove: NetLabelPlacement) {
186
+ const labelIndex = this.outputNetLabelPlacements.findIndex(
187
+ (label) =>
188
+ label.globalConnNetId === labelToMove.globalConnNetId &&
189
+ label.anchorPoint.x === labelToMove.anchorPoint.x &&
190
+ label.anchorPoint.y === labelToMove.anchorPoint.y &&
191
+ label.pinIds.join(",") === labelToMove.pinIds.join(","),
192
+ )
193
+ if (labelIndex === -1) return false
194
+
195
+ const label = this.outputNetLabelPlacements[labelIndex]!
196
+ if (label.mspConnectionPairIds.length > 0) return false
197
+ if (label.pinIds.length !== 1) return false
198
+ if (
199
+ this.outputNetLabelPlacements.filter(
200
+ (otherLabel) => otherLabel.globalConnNetId === label.globalConnNetId,
201
+ ).length !== 1
202
+ ) {
203
+ return false
204
+ }
205
+
206
+ const outward = dir(label.orientation)
207
+ if (outward.x === 0 && outward.y === 0) return false
208
+
209
+ for (let step = 1; step <= LABEL_MAX_OUTWARD_STEPS; step++) {
210
+ const distance = step * LABEL_OUTWARD_STEP
211
+ const candidate = {
212
+ ...label,
213
+ anchorPoint: {
214
+ x: label.anchorPoint.x + outward.x * distance,
215
+ y: label.anchorPoint.y + outward.y * distance,
216
+ },
217
+ center: {
218
+ x: label.center.x + outward.x * distance,
219
+ y: label.center.y + outward.y * distance,
220
+ },
221
+ }
222
+ const candidateWithClearance = {
223
+ ...candidate,
224
+ width: candidate.width + LABEL_TRACE_CLEARANCE * 2,
225
+ height: candidate.height + LABEL_TRACE_CLEARANCE * 2,
226
+ }
227
+ if (
228
+ detectTraceLabelOverlap({
229
+ traces: this.outputTraces,
230
+ netLabels: [candidateWithClearance],
231
+ }).length > 0
232
+ ) {
233
+ continue
234
+ }
235
+
236
+ const connectorTrace = createPortOnlyLabelConnectorTrace({
237
+ label,
238
+ movedLabel: candidate,
239
+ pinMap: this.pinMap,
240
+ })
241
+ if (
242
+ detectTraceLabelOverlap({
243
+ traces: [connectorTrace],
244
+ netLabels: this.outputNetLabelPlacements,
245
+ }).length > 0
246
+ ) {
247
+ continue
248
+ }
249
+
250
+ this.outputNetLabelPlacements[labelIndex] = candidate
251
+ this.outputTraces.push(connectorTrace)
252
+ return true
253
+ }
254
+
255
+ return false
256
+ }
257
+
172
258
  override visualize(): GraphicsObject {
173
259
  return visualizeExample28Solver({
174
260
  inputProblem: this.inputProblem,
@@ -180,3 +266,29 @@ export class Example28Solver extends BaseSolver {
180
266
  })
181
267
  }
182
268
  }
269
+
270
+ const getPortOnlyLabelConnectorMspPairId = (label: NetLabelPlacement) =>
271
+ `port-only-label-connector-${label.globalConnNetId}-${label.pinIds[0]}`
272
+
273
+ const createPortOnlyLabelConnectorTrace = ({
274
+ label,
275
+ movedLabel,
276
+ pinMap,
277
+ }: {
278
+ label: NetLabelPlacement
279
+ movedLabel: NetLabelPlacement
280
+ pinMap: Record<string, InputPin & { chipId: string }>
281
+ }): SolvedTracePath => {
282
+ const mspPairId = getPortOnlyLabelConnectorMspPairId(label)
283
+
284
+ return {
285
+ mspPairId,
286
+ dcConnNetId: label.dcConnNetId ?? label.globalConnNetId,
287
+ globalConnNetId: label.globalConnNetId,
288
+ userNetId: label.netId,
289
+ pins: getTracePins(label, pinMap),
290
+ tracePath: [label.anchorPoint, movedLabel.anchorPoint],
291
+ mspConnectionPairIds: [mspPairId],
292
+ pinIds: label.pinIds,
293
+ }
294
+ }