@tscircuit/schematic-trace-solver 0.0.140 → 0.0.142

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -115,6 +115,16 @@ interface InputNetConnection {
115
115
  pinIds: Array<PinId>;
116
116
  netLabelWidth?: number;
117
117
  netLabelHeight?: number;
118
+ /**
119
+ * When true, a named single-pin net may be drawn as a short outward trace
120
+ * stub with its net name placed inline. Multi-pin net connections retain the
121
+ * regular anchored-label behavior.
122
+ */
123
+ allowInlineNetLabel?: boolean;
124
+ /** Extent of the inline text along the generated trace stub. */
125
+ inlineNetLabelWidth?: number;
126
+ /** Height of the inline text perpendicular to the generated trace stub. */
127
+ inlineNetLabelHeight?: number;
118
128
  }
119
129
  interface InputProblem {
120
130
  chips: Array<InputChip>;
@@ -279,6 +289,7 @@ declare class TraceOverlapIssueSolver extends BaseSolver {
279
289
  }
280
290
 
281
291
  type ConnNetId = string;
292
+ type TraceState = Record<MspConnectionPairId, SolvedTracePath>;
282
293
  /**
283
294
  * This solver finds traces that overlap or meet collinearly and aren't
284
295
  * connected via the globalConnMap, then shifts them apart in the direction
@@ -308,6 +319,7 @@ declare class TraceOverlapShiftSolver extends BaseSolver {
308
319
  */
309
320
  traceNetIslands: Record<ConnNetId, Array<SolvedTracePath>>;
310
321
  correctedTraceMap: Record<MspConnectionPairId, SolvedTracePath>;
322
+ recentTraceStates: TraceState[];
311
323
  cleanupPhase: "diagonals" | "done" | null;
312
324
  constructor(params: {
313
325
  inputProblem: InputProblem;
@@ -324,6 +336,8 @@ declare class TraceOverlapShiftSolver extends BaseSolver {
324
336
  private findNextDiagonalSegment;
325
337
  private findAndFixNextDiagonalSegment;
326
338
  _step(): void;
339
+ private rememberTraceState;
340
+ private returnsToPreviousTraceState;
327
341
  visualize(): graphics_debug.GraphicsObject;
328
342
  }
329
343
 
@@ -1182,8 +1196,13 @@ declare const INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4;
1182
1196
  interface InlineNetLabelPlacement {
1183
1197
  globalConnNetId: string;
1184
1198
  netId?: string;
1185
- mspPairId: string;
1199
+ mspPairId?: string;
1186
1200
  pinIds: PinId[];
1201
+ /**
1202
+ * A generated single-ended trace stub. Present only for an eligible
1203
+ * single-pin net connection; routed point-to-point traces omit it.
1204
+ */
1205
+ stubTracePath?: [Point, Point];
1187
1206
  /** Axis the text runs along: "x" reads left-to-right, "y" reads bottom-to-top */
1188
1207
  axis: "x" | "y";
1189
1208
  /** Midpoint of the trace segment the label is attached to */
@@ -1220,10 +1239,20 @@ declare class InlineNetLabelSolver extends BaseSolver {
1220
1239
  inlineNetLabelPlacements: InlineNetLabelPlacement[];
1221
1240
  /** Direct connections that opted in, still waiting to be processed */
1222
1241
  queuedDirectConnections: InputDirectConnection[];
1242
+ /** Single-pin net connections that opted in, still waiting to be processed */
1243
+ queuedPortOnlyNetConnections: InputNetConnection[];
1223
1244
  private tracesByPinPairKey;
1245
+ private hasAlignedPortOnlyStubs;
1224
1246
  constructor(input: InlineNetLabelSolverInput);
1225
1247
  getConstructorParams(): [InlineNetLabelSolverInput];
1226
1248
  _step(): void;
1249
+ /**
1250
+ * Converts a conventional port-only anchored placement into an inline label
1251
+ * on a generated outward stub. The stub follows the pin's true facing
1252
+ * direction; an anchored label may finish in another direction after an
1253
+ * elbow, which is not the direction a terminal stub should leave the pin.
1254
+ */
1255
+ private computePortOnlyInlinePlacement;
1227
1256
  private computeInlinePlacement;
1228
1257
  /**
1229
1258
  * Places the label over the whole route when the route is straight except
@@ -1242,6 +1271,7 @@ declare class InlineNetLabelSolver extends BaseSolver {
1242
1271
  * the other, never both.
1243
1272
  */
1244
1273
  private getSupersededNetLabelKeys;
1274
+ private getOutputTraces;
1245
1275
  getOutput(): {
1246
1276
  traces: SolvedTracePath[];
1247
1277
  netLabelPlacements: NetLabelPlacement[];
package/dist/index.js CHANGED
@@ -2102,6 +2102,7 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
2102
2102
  };
2103
2103
 
2104
2104
  // lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts
2105
+ var TRACE_STATE_POSITION_EPSILON = 1e-6;
2105
2106
  var TraceOverlapShiftSolver = class extends BaseSolver {
2106
2107
  inputProblem;
2107
2108
  inputTracePaths;
@@ -2112,6 +2113,9 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
2112
2113
  */
2113
2114
  traceNetIslands = {};
2114
2115
  correctedTraceMap = {};
2116
+ // Keep only the current and previous layouts to detect a two-state cycle
2117
+ // without accumulating routing history for the whole solve.
2118
+ recentTraceStates = [];
2115
2119
  cleanupPhase = null;
2116
2120
  constructor(params) {
2117
2121
  super();
@@ -2123,6 +2127,7 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
2123
2127
  const { mspPairId } = tracePath;
2124
2128
  this.correctedTraceMap[mspPairId] = tracePath;
2125
2129
  }
2130
+ this.rememberTraceState(this.correctedTraceMap);
2126
2131
  this.traceNetIslands = this.computeTraceNetIslands();
2127
2132
  }
2128
2133
  getConstructorParams() {
@@ -2337,11 +2342,17 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
2337
2342
  }
2338
2343
  _step() {
2339
2344
  if (this.activeSubSolver?.solved) {
2340
- for (const [mspPairId, newTrace] of Object.entries(
2341
- this.activeSubSolver.correctedTraceMap
2342
- )) {
2343
- this.correctedTraceMap[mspPairId] = newTrace;
2345
+ const nextTraceState = {
2346
+ ...this.correctedTraceMap,
2347
+ ...this.activeSubSolver.correctedTraceMap
2348
+ };
2349
+ if (this.returnsToPreviousTraceState(nextTraceState)) {
2350
+ this.activeSubSolver = null;
2351
+ this.solved = true;
2352
+ return;
2344
2353
  }
2354
+ this.correctedTraceMap = nextTraceState;
2355
+ this.rememberTraceState(nextTraceState);
2345
2356
  this.activeSubSolver = null;
2346
2357
  this.traceNetIslands = this.computeTraceNetIslands();
2347
2358
  }
@@ -2374,6 +2385,34 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
2374
2385
  traceIdsToShift: this.traceIdsToShift
2375
2386
  });
2376
2387
  }
2388
+ rememberTraceState(traceState) {
2389
+ this.recentTraceStates.push({ ...traceState });
2390
+ if (this.recentTraceStates.length > 2) {
2391
+ this.recentTraceStates.shift();
2392
+ }
2393
+ }
2394
+ returnsToPreviousTraceState(candidateTraceState) {
2395
+ if (this.recentTraceStates.length < 2) return false;
2396
+ const previousTraceState = this.recentTraceStates[0];
2397
+ const traceIds = Object.keys(candidateTraceState);
2398
+ if (traceIds.length !== Object.keys(previousTraceState).length) return false;
2399
+ for (const traceId of traceIds) {
2400
+ const candidatePath = candidateTraceState[traceId];
2401
+ const previousPath = previousTraceState[traceId];
2402
+ if (!candidatePath || !previousPath) return false;
2403
+ if (candidatePath.tracePath.length !== previousPath.tracePath.length) {
2404
+ return false;
2405
+ }
2406
+ for (let i = 0; i < candidatePath.tracePath.length; i++) {
2407
+ const candidatePoint = candidatePath.tracePath[i];
2408
+ const previousPoint = previousPath.tracePath[i];
2409
+ if (Math.abs(candidatePoint.x - previousPoint.x) > TRACE_STATE_POSITION_EPSILON || Math.abs(candidatePoint.y - previousPoint.y) > TRACE_STATE_POSITION_EPSILON) {
2410
+ return false;
2411
+ }
2412
+ }
2413
+ }
2414
+ return true;
2415
+ }
2377
2416
  visualize() {
2378
2417
  if (this.activeSubSolver) {
2379
2418
  return this.activeSubSolver.visualize();
@@ -12537,6 +12576,154 @@ var getAxisAlignedSegments = (path, epsilon = 1e-6) => {
12537
12576
  return segments.sort((a, b) => b.length - a.length);
12538
12577
  };
12539
12578
 
12579
+ // lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts
12580
+ var getStubDirection = (path) => {
12581
+ const [start, end] = path;
12582
+ if (Math.abs(end.x - start.x) >= Math.abs(end.y - start.y)) {
12583
+ return end.x >= start.x ? "x+" : "x-";
12584
+ }
12585
+ return end.y >= start.y ? "y+" : "y-";
12586
+ };
12587
+ var getStubLength = (path) => {
12588
+ const [start, end] = path;
12589
+ return Math.abs(end.x - start.x) + Math.abs(end.y - start.y);
12590
+ };
12591
+ var getLabelBounds2 = (placement) => {
12592
+ const isVertical5 = placement.axis === "y";
12593
+ const width = isVertical5 ? placement.height : placement.width;
12594
+ const height = isVertical5 ? placement.width : placement.height;
12595
+ return {
12596
+ minX: placement.center.x - width / 2,
12597
+ maxX: placement.center.x + width / 2,
12598
+ minY: placement.center.y - height / 2,
12599
+ maxY: placement.center.y + height / 2
12600
+ };
12601
+ };
12602
+ var getPathBounds = (path) => ({
12603
+ minX: Math.min(...path.map((point) => point.x)),
12604
+ maxX: Math.max(...path.map((point) => point.x)),
12605
+ minY: Math.min(...path.map((point) => point.y)),
12606
+ maxY: Math.max(...path.map((point) => point.y))
12607
+ });
12608
+ var doesPathIntersectBounds = (path, bounds) => {
12609
+ for (let index = 0; index < path.length - 1; index++) {
12610
+ const segmentBounds = getPathBounds([path[index], path[index + 1]]);
12611
+ if (boundsOverlap(segmentBounds, bounds)) return true;
12612
+ }
12613
+ return false;
12614
+ };
12615
+ var resizeStub = (placement, direction, targetLength) => {
12616
+ const [start] = placement.stubTracePath;
12617
+ const end = direction === "x+" ? { x: start.x + targetLength, y: start.y } : direction === "x-" ? { x: start.x - targetLength, y: start.y } : direction === "y+" ? { x: start.x, y: start.y + targetLength } : { x: start.x, y: start.y - targetLength };
12618
+ const anchorPoint = {
12619
+ x: (start.x + end.x) / 2,
12620
+ y: (start.y + end.y) / 2
12621
+ };
12622
+ return {
12623
+ ...placement,
12624
+ stubTracePath: [start, end],
12625
+ anchorPoint,
12626
+ // Extending a short row only adds wire at its free end. Keep the label
12627
+ // itself next to the pin so terminal labels can be start/end aligned and
12628
+ // collision checks continue to describe the rendered text box.
12629
+ center: placement.center
12630
+ };
12631
+ };
12632
+ var alignPortOnlyInlineNetLabelStubs = ({
12633
+ placements,
12634
+ inputProblem,
12635
+ traces,
12636
+ netLabelPlacements
12637
+ }) => {
12638
+ const chipIdByPinId = /* @__PURE__ */ new Map();
12639
+ for (const chip of inputProblem.chips) {
12640
+ for (const pin of chip.pins) chipIdByPinId.set(pin.pinId, chip.chipId);
12641
+ }
12642
+ const groups = /* @__PURE__ */ new Map();
12643
+ for (const [placementIndex, placement] of placements.entries()) {
12644
+ if (!placement.stubTracePath || placement.pinIds.length !== 1) continue;
12645
+ const direction = getStubDirection(placement.stubTracePath);
12646
+ if (!direction) continue;
12647
+ const ownerChipId = chipIdByPinId.get(placement.pinIds[0]);
12648
+ const groupKey = `${ownerChipId ?? placement.pinIds[0]}::${direction}`;
12649
+ const group = groups.get(groupKey) ?? [];
12650
+ group.push({ placementIndex, placement, direction, ownerChipId });
12651
+ groups.set(groupKey, group);
12652
+ }
12653
+ const alignedPlacements = [...placements];
12654
+ const supersededGlobalNetIds = new Set(
12655
+ placements.map((placement) => placement.globalConnNetId)
12656
+ );
12657
+ const retainedAnchoredLabelBounds = netLabelPlacements.filter(
12658
+ (placement) => !supersededGlobalNetIds.has(placement.globalConnNetId)
12659
+ ).map(getLabelBounds2);
12660
+ for (const group of groups.values()) {
12661
+ if (group.length < 2) continue;
12662
+ const targetLength = Math.max(
12663
+ ...group.map(({ placement }) => getStubLength(placement.stubTracePath))
12664
+ );
12665
+ const proposals = group.map(
12666
+ ({ placement, direction }) => resizeStub(placement, direction, targetLength)
12667
+ );
12668
+ const groupPlacementIndices = new Set(
12669
+ group.map(({ placementIndex }) => placementIndex)
12670
+ );
12671
+ const fixedInlinePlacements = placements.filter(
12672
+ (_, placementIndex) => !groupPlacementIndices.has(placementIndex)
12673
+ );
12674
+ const hasConflict = proposals.some((proposal, proposalIndex) => {
12675
+ const labelBounds = getLabelBounds2(proposal);
12676
+ const stubPath = proposal.stubTracePath;
12677
+ const stubBounds = getPathBounds(stubPath);
12678
+ const ownerChipId = group[proposalIndex].ownerChipId;
12679
+ for (const chip of inputProblem.chips) {
12680
+ const chipBounds = {
12681
+ minX: chip.center.x - chip.width / 2,
12682
+ maxX: chip.center.x + chip.width / 2,
12683
+ minY: chip.center.y - chip.height / 2,
12684
+ maxY: chip.center.y + chip.height / 2
12685
+ };
12686
+ if (boundsOverlap(labelBounds, chipBounds)) return true;
12687
+ if (chip.chipId !== ownerChipId && boundsOverlap(stubBounds, chipBounds))
12688
+ return true;
12689
+ }
12690
+ for (const textBox of inputProblem.textBoxes ?? []) {
12691
+ const textBounds = getTextBoxBounds(textBox);
12692
+ if (boundsOverlap(labelBounds, textBounds) || boundsOverlap(stubBounds, textBounds))
12693
+ return true;
12694
+ }
12695
+ for (const trace of traces) {
12696
+ if (trace.globalConnNetId === proposal.globalConnNetId) continue;
12697
+ if (doesPathIntersectBounds(trace.tracePath, labelBounds) || doesPathIntersectBounds(trace.tracePath, stubBounds))
12698
+ return true;
12699
+ }
12700
+ if (retainedAnchoredLabelBounds.some(
12701
+ (bounds) => boundsOverlap(labelBounds, bounds) || boundsOverlap(stubBounds, bounds)
12702
+ ))
12703
+ return true;
12704
+ for (const fixedPlacement of fixedInlinePlacements) {
12705
+ const fixedLabelBounds = getLabelBounds2(fixedPlacement);
12706
+ if (boundsOverlap(labelBounds, fixedLabelBounds) || boundsOverlap(stubBounds, fixedLabelBounds))
12707
+ return true;
12708
+ if (fixedPlacement.stubTracePath && (doesPathIntersectBounds(fixedPlacement.stubTracePath, labelBounds) || doesPathIntersectBounds(stubPath, fixedLabelBounds)))
12709
+ return true;
12710
+ }
12711
+ for (const [otherIndex, otherProposal] of proposals.entries()) {
12712
+ if (otherIndex === proposalIndex) continue;
12713
+ const otherLabelBounds = getLabelBounds2(otherProposal);
12714
+ if (boundsOverlap(labelBounds, otherLabelBounds)) return true;
12715
+ if (doesPathIntersectBounds(stubPath, otherLabelBounds)) return true;
12716
+ }
12717
+ return false;
12718
+ });
12719
+ if (hasConflict) continue;
12720
+ for (const [groupIndex, { placementIndex }] of group.entries()) {
12721
+ alignedPlacements[placementIndex] = proposals[groupIndex];
12722
+ }
12723
+ }
12724
+ return alignedPlacements;
12725
+ };
12726
+
12540
12727
  // lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts
12541
12728
  var DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
12542
12729
  var INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
@@ -12549,7 +12736,10 @@ var InlineNetLabelSolver = class extends BaseSolver {
12549
12736
  inlineNetLabelPlacements = [];
12550
12737
  /** Direct connections that opted in, still waiting to be processed */
12551
12738
  queuedDirectConnections;
12739
+ /** Single-pin net connections that opted in, still waiting to be processed */
12740
+ queuedPortOnlyNetConnections;
12552
12741
  tracesByPinPairKey;
12742
+ hasAlignedPortOnlyStubs = false;
12553
12743
  constructor(input) {
12554
12744
  super();
12555
12745
  this.inputProblem = input.inputProblem;
@@ -12558,6 +12748,9 @@ var InlineNetLabelSolver = class extends BaseSolver {
12558
12748
  this.queuedDirectConnections = this.inputProblem.directConnections.filter(
12559
12749
  (dc) => dc.allowInlineNetLabel && dc.netId
12560
12750
  );
12751
+ this.queuedPortOnlyNetConnections = this.inputProblem.netConnections.filter(
12752
+ (nc) => nc.allowInlineNetLabel && nc.pinIds.length === 1 && nc.netId
12753
+ );
12561
12754
  this.tracesByPinPairKey = /* @__PURE__ */ new Map();
12562
12755
  for (const trace of this.traces) {
12563
12756
  const key = getPinPairKey2(trace.pins.map((p) => p.pinId));
@@ -12580,15 +12773,97 @@ var InlineNetLabelSolver = class extends BaseSolver {
12580
12773
  }
12581
12774
  _step() {
12582
12775
  const directConnection = this.queuedDirectConnections.shift();
12583
- if (!directConnection) {
12584
- this.solved = true;
12585
- this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length;
12776
+ if (directConnection) {
12777
+ const placement = this.computeInlinePlacement(directConnection);
12778
+ if (placement) {
12779
+ this.inlineNetLabelPlacements.push(placement);
12780
+ }
12586
12781
  return;
12587
12782
  }
12588
- const placement = this.computeInlinePlacement(directConnection);
12589
- if (placement) {
12590
- this.inlineNetLabelPlacements.push(placement);
12783
+ const portOnlyNetConnection = this.queuedPortOnlyNetConnections.shift();
12784
+ if (portOnlyNetConnection) {
12785
+ const placement = this.computePortOnlyInlinePlacement(
12786
+ portOnlyNetConnection
12787
+ );
12788
+ if (placement) {
12789
+ this.inlineNetLabelPlacements.push(placement);
12790
+ }
12791
+ return;
12792
+ }
12793
+ if (!this.hasAlignedPortOnlyStubs) {
12794
+ this.inlineNetLabelPlacements = alignPortOnlyInlineNetLabelStubs({
12795
+ placements: this.inlineNetLabelPlacements,
12796
+ inputProblem: this.inputProblem,
12797
+ traces: this.traces,
12798
+ netLabelPlacements: this.inputNetLabelPlacements
12799
+ });
12800
+ this.hasAlignedPortOnlyStubs = true;
12801
+ return;
12802
+ }
12803
+ this.solved = true;
12804
+ this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length;
12805
+ }
12806
+ /**
12807
+ * Converts a conventional port-only anchored placement into an inline label
12808
+ * on a generated outward stub. The stub follows the pin's true facing
12809
+ * direction; an anchored label may finish in another direction after an
12810
+ * elbow, which is not the direction a terminal stub should leave the pin.
12811
+ */
12812
+ computePortOnlyInlinePlacement(netConnection) {
12813
+ const [pinId] = netConnection.pinIds;
12814
+ if (!pinId) return null;
12815
+ const anchoredPlacement = this.inputNetLabelPlacements.find(
12816
+ (placement) => placement.netId === netConnection.netId && placement.pinIds.length === 1 && placement.pinIds[0] === pinId
12817
+ );
12818
+ if (!anchoredPlacement) return null;
12819
+ const inputChip = this.inputProblem.chips.find(
12820
+ (chip) => chip.pins.some((pin) => pin.pinId === pinId)
12821
+ );
12822
+ const inputPin = inputChip?.pins.find((pin) => pin.pinId === pinId);
12823
+ const height = netConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
12824
+ const width = netConnection.inlineNetLabelWidth ?? netConnection.netLabelWidth ?? estimateInlineNetLabelWidth(netConnection.netId, height);
12825
+ const stubLength = Math.max(width + 0.2, 0.6);
12826
+ const start = inputPin ? { x: inputPin.x, y: inputPin.y } : anchoredPlacement.anchorPoint;
12827
+ const direction = inputPin && inputChip ? inputPin._facingDirection ?? getPinDirection(inputPin, inputChip) : anchoredPlacement.orientation;
12828
+ const end = direction === "x+" ? { x: start.x + stubLength, y: start.y } : direction === "x-" ? { x: start.x - stubLength, y: start.y } : direction === "y+" ? { x: start.x, y: start.y + stubLength } : { x: start.x, y: start.y - stubLength };
12829
+ const axis = direction === "x+" || direction === "x-" ? "x" : "y";
12830
+ const side = axis === "x" ? "y+" : "x-";
12831
+ const anchorPoint = {
12832
+ x: (start.x + end.x) / 2,
12833
+ y: (start.y + end.y) / 2
12834
+ };
12835
+ const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN;
12836
+ const center = side === "y+" ? { x: anchorPoint.x, y: anchorPoint.y + offset } : { x: anchorPoint.x - offset, y: anchorPoint.y };
12837
+ const halfAlong = width / 2;
12838
+ const halfAcross = height / 2;
12839
+ const bounds = axis === "x" ? {
12840
+ minX: center.x - halfAlong,
12841
+ maxX: center.x + halfAlong,
12842
+ minY: center.y - halfAcross,
12843
+ maxY: center.y + halfAcross
12844
+ } : {
12845
+ minX: center.x - halfAcross,
12846
+ maxX: center.x + halfAcross,
12847
+ minY: center.y - halfAlong,
12848
+ maxY: center.y + halfAlong
12849
+ };
12850
+ if (this.isObstructed(bounds, {
12851
+ ownGlobalConnNetId: anchoredPlacement.globalConnNetId
12852
+ })) {
12853
+ return null;
12591
12854
  }
12855
+ return {
12856
+ globalConnNetId: anchoredPlacement.globalConnNetId,
12857
+ netId: netConnection.netId,
12858
+ pinIds: [pinId],
12859
+ stubTracePath: [start, end],
12860
+ axis,
12861
+ anchorPoint,
12862
+ center,
12863
+ width,
12864
+ height,
12865
+ side
12866
+ };
12592
12867
  }
12593
12868
  computeInlinePlacement(directConnection) {
12594
12869
  const traces = this.tracesByPinPairKey.get(getPinPairKey2(directConnection.pinIds)) ?? [];
@@ -12618,7 +12893,11 @@ var InlineNetLabelSolver = class extends BaseSolver {
12618
12893
  minY: center.y - halfAlong,
12619
12894
  maxY: center.y + halfAlong
12620
12895
  };
12621
- if (this.isObstructed(bounds, trace)) continue;
12896
+ if (this.isObstructed(bounds, {
12897
+ ownTrace: trace,
12898
+ ownGlobalConnNetId: trace.globalConnNetId
12899
+ }))
12900
+ continue;
12622
12901
  return {
12623
12902
  globalConnNetId: trace.globalConnNetId,
12624
12903
  netId: directConnection.netId,
@@ -12703,7 +12982,11 @@ var InlineNetLabelSolver = class extends BaseSolver {
12703
12982
  minY: center.y - halfAlong,
12704
12983
  maxY: center.y + halfAlong
12705
12984
  };
12706
- if (this.isObstructed(bounds, trace)) continue;
12985
+ if (this.isObstructed(bounds, {
12986
+ ownTrace: trace,
12987
+ ownGlobalConnNetId: trace.globalConnNetId
12988
+ }))
12989
+ continue;
12707
12990
  const anchorPoint = axis === "x" ? { x: along, y: side === "y+" ? maxY : minY } : { x: side === "x-" ? minX : maxX, y: along };
12708
12991
  return {
12709
12992
  globalConnNetId: trace.globalConnNetId,
@@ -12725,7 +13008,10 @@ var InlineNetLabelSolver = class extends BaseSolver {
12725
13008
  * An inline label may not sit on top of a chip, a component's text, or a
12726
13009
  * trace belonging to another net.
12727
13010
  */
12728
- isObstructed(bounds, ownTrace) {
13011
+ isObstructed(bounds, {
13012
+ ownTrace,
13013
+ ownGlobalConnNetId
13014
+ }) {
12729
13015
  for (const chip of this.inputProblem.chips) {
12730
13016
  const chipBounds = {
12731
13017
  minX: chip.center.x - chip.width / 2,
@@ -12739,9 +13025,9 @@ var InlineNetLabelSolver = class extends BaseSolver {
12739
13025
  if (boundsOverlap(bounds, getTextBoxBounds(textBox))) return true;
12740
13026
  }
12741
13027
  for (const trace of this.traces) {
12742
- if (trace.mspPairId === ownTrace.mspPairId) continue;
12743
- if (trace.globalConnNetId === ownTrace.globalConnNetId) continue;
12744
- if (doesPathIntersectBounds(trace.tracePath, bounds)) return true;
13028
+ if (ownTrace && trace.mspPairId === ownTrace.mspPairId) continue;
13029
+ if (trace.globalConnNetId === ownGlobalConnNetId) continue;
13030
+ if (doesPathIntersectBounds2(trace.tracePath, bounds)) return true;
12745
13031
  }
12746
13032
  return false;
12747
13033
  }
@@ -12756,10 +13042,18 @@ var InlineNetLabelSolver = class extends BaseSolver {
12756
13042
  }
12757
13043
  return keys;
12758
13044
  }
13045
+ getOutputTraces(supersededNetLabelKeys) {
13046
+ return this.traces.filter(
13047
+ (trace) => !(supersededNetLabelKeys.has(trace.globalConnNetId) && trace.mspPairId.startsWith("available-net-orientation-"))
13048
+ );
13049
+ }
12759
13050
  getOutput() {
12760
13051
  const superseded = this.getSupersededNetLabelKeys();
12761
13052
  return {
12762
- traces: this.traces,
13053
+ // AvailableNetOrientationSolver may have routed an elbow from a port to
13054
+ // the anchored label that this inline placement supersedes. Keep the
13055
+ // actual net trace, but discard that now-orphaned label connector.
13056
+ traces: this.getOutputTraces(superseded),
12763
13057
  netLabelPlacements: this.inputNetLabelPlacements.filter(
12764
13058
  (placement) => !superseded.has(placement.globalConnNetId)
12765
13059
  ),
@@ -12772,7 +13066,8 @@ var InlineNetLabelSolver = class extends BaseSolver {
12772
13066
  graphics.rects ??= [];
12773
13067
  graphics.points ??= [];
12774
13068
  graphics.texts ??= [];
12775
- for (const trace of this.traces) {
13069
+ const output = this.getOutput();
13070
+ for (const trace of output.traces) {
12776
13071
  graphics.lines.push({
12777
13072
  points: trace.tracePath,
12778
13073
  strokeColor: "purple"
@@ -12798,6 +13093,12 @@ orientation: ${label.orientation}`
12798
13093
  });
12799
13094
  }
12800
13095
  for (const inlineLabel of this.inlineNetLabelPlacements) {
13096
+ if (inlineLabel.stubTracePath) {
13097
+ graphics.lines.push({
13098
+ points: inlineLabel.stubTracePath,
13099
+ strokeColor: "purple"
13100
+ });
13101
+ }
12801
13102
  const isHorizontal4 = inlineLabel.axis === "x";
12802
13103
  graphics.rects.push({
12803
13104
  center: inlineLabel.center,
@@ -12812,12 +13113,12 @@ orientation: ${label.orientation}`
12812
13113
  ].join("\n")
12813
13114
  });
12814
13115
  graphics.texts.push({
12815
- x: inlineLabel.center.x,
12816
- y: inlineLabel.center.y,
13116
+ x: inlineLabel.stubTracePath && inlineLabel.axis === "x" ? inlineLabel.center.x + (inlineLabel.stubTracePath[1].x > inlineLabel.stubTracePath[0].x ? -inlineLabel.width / 2 : inlineLabel.width / 2) : inlineLabel.center.x,
13117
+ y: inlineLabel.stubTracePath && inlineLabel.axis === "y" ? inlineLabel.center.y + (inlineLabel.stubTracePath[1].y > inlineLabel.stubTracePath[0].y ? -inlineLabel.width / 2 : inlineLabel.width / 2) : inlineLabel.center.y,
12817
13118
  text: inlineLabel.netId ?? "",
12818
13119
  color: "green",
12819
13120
  fontSize: inlineLabel.height,
12820
- anchorSide: "center",
13121
+ anchorSide: inlineLabel.stubTracePath ? inlineLabel.stubTracePath[1][inlineLabel.axis] > inlineLabel.stubTracePath[0][inlineLabel.axis] ? "center_left" : "center_right" : "center",
12821
13122
  // Vertical labels read bottom-to-top, alongside the wire they name.
12822
13123
  rotation: inlineLabel.axis === "y" ? 90 : void 0
12823
13124
  });
@@ -12852,7 +13153,7 @@ var getAnchorCandidates = (segment, labelWidth, step = 0.1) => {
12852
13153
  offsets.push(slack / 2, -slack / 2);
12853
13154
  return offsets.map((offset) => pointAt(mid + offset * direction));
12854
13155
  };
12855
- var doesPathIntersectBounds = (path, bounds) => {
13156
+ var doesPathIntersectBounds2 = (path, bounds) => {
12856
13157
  for (let i = 0; i < path.length - 1; i++) {
12857
13158
  const a = path[i];
12858
13159
  const b = path[i + 1];