@tscircuit/schematic-trace-solver 0.0.141 → 0.0.143

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>;
@@ -815,6 +825,7 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
815
825
  private findValidTraceAnchorCandidate;
816
826
  private findValidOutwardTraceAnchorCandidate;
817
827
  private getTraceAnchorCandidatePoints;
828
+ private sharesVerticalRailWithAny;
818
829
  private getAvailableOrientations;
819
830
  private findValidShiftedCandidate;
820
831
  /**
@@ -1186,8 +1197,13 @@ declare const INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4;
1186
1197
  interface InlineNetLabelPlacement {
1187
1198
  globalConnNetId: string;
1188
1199
  netId?: string;
1189
- mspPairId: string;
1200
+ mspPairId?: string;
1190
1201
  pinIds: PinId[];
1202
+ /**
1203
+ * A generated single-ended trace stub. Present only for an eligible
1204
+ * single-pin net connection; routed point-to-point traces omit it.
1205
+ */
1206
+ stubTracePath?: [Point, Point];
1191
1207
  /** Axis the text runs along: "x" reads left-to-right, "y" reads bottom-to-top */
1192
1208
  axis: "x" | "y";
1193
1209
  /** Midpoint of the trace segment the label is attached to */
@@ -1224,10 +1240,20 @@ declare class InlineNetLabelSolver extends BaseSolver {
1224
1240
  inlineNetLabelPlacements: InlineNetLabelPlacement[];
1225
1241
  /** Direct connections that opted in, still waiting to be processed */
1226
1242
  queuedDirectConnections: InputDirectConnection[];
1243
+ /** Single-pin net connections that opted in, still waiting to be processed */
1244
+ queuedPortOnlyNetConnections: InputNetConnection[];
1227
1245
  private tracesByPinPairKey;
1246
+ private hasAlignedPortOnlyStubs;
1228
1247
  constructor(input: InlineNetLabelSolverInput);
1229
1248
  getConstructorParams(): [InlineNetLabelSolverInput];
1230
1249
  _step(): void;
1250
+ /**
1251
+ * Converts a conventional port-only anchored placement into an inline label
1252
+ * on a generated outward stub. The stub follows the pin's true facing
1253
+ * direction; an anchored label may finish in another direction after an
1254
+ * elbow, which is not the direction a terminal stub should leave the pin.
1255
+ */
1256
+ private computePortOnlyInlinePlacement;
1231
1257
  private computeInlinePlacement;
1232
1258
  /**
1233
1259
  * Places the label over the whole route when the route is straight except
@@ -1246,6 +1272,7 @@ declare class InlineNetLabelSolver extends BaseSolver {
1246
1272
  * the other, never both.
1247
1273
  */
1248
1274
  private getSupersededNetLabelKeys;
1275
+ private getOutputTraces;
1249
1276
  getOutput(): {
1250
1277
  traces: SolvedTracePath[];
1251
1278
  netLabelPlacements: NetLabelPlacement[];
package/dist/index.js CHANGED
@@ -8842,13 +8842,14 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8842
8842
  }
8843
8843
  findValidTraceAnchorCandidate(label, orientation, labelIndex) {
8844
8844
  const direction = dir(orientation);
8845
- const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
8846
- (a, b) => {
8847
- const aAlongDirection = a.x * direction.x + a.y * direction.y;
8848
- const bAlongDirection = b.x * direction.x + b.y * direction.y;
8849
- return bAlongDirection - aAlongDirection;
8850
- }
8851
- );
8845
+ const candidatePoints = this.getTraceAnchorCandidatePoints(
8846
+ label,
8847
+ orientation
8848
+ ).sort((a, b) => {
8849
+ const aAlongDirection = a.x * direction.x + a.y * direction.y;
8850
+ const bAlongDirection = b.x * direction.x + b.y * direction.y;
8851
+ return bAlongDirection - aAlongDirection;
8852
+ });
8852
8853
  for (const anchorPoint of candidatePoints) {
8853
8854
  const candidate = this.createCandidate(label, anchorPoint, orientation);
8854
8855
  const result = this.evaluateCandidate(
@@ -8867,13 +8868,14 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8867
8868
  }
8868
8869
  findValidOutwardTraceAnchorCandidate(label, orientation, labelIndex) {
8869
8870
  const direction = dir(orientation);
8870
- const candidatePoints = this.getTraceAnchorCandidatePoints(label).sort(
8871
- (a, b) => {
8872
- const aAlongDirection = a.x * direction.x + a.y * direction.y;
8873
- const bAlongDirection = b.x * direction.x + b.y * direction.y;
8874
- return bAlongDirection - aAlongDirection;
8875
- }
8876
- );
8871
+ const candidatePoints = this.getTraceAnchorCandidatePoints(
8872
+ label,
8873
+ orientation
8874
+ ).sort((a, b) => {
8875
+ const aAlongDirection = a.x * direction.x + a.y * direction.y;
8876
+ const bAlongDirection = b.x * direction.x + b.y * direction.y;
8877
+ return bAlongDirection - aAlongDirection;
8878
+ });
8877
8879
  for (const connectorSource of candidatePoints) {
8878
8880
  const preservedColumnAnchor = this.getSearchStartAnchor(
8879
8881
  label,
@@ -8931,10 +8933,40 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8931
8933
  }
8932
8934
  return null;
8933
8935
  }
8934
- getTraceAnchorCandidatePoints(label) {
8936
+ getTraceAnchorCandidatePoints(label, orientation) {
8935
8937
  const seen = /* @__PURE__ */ new Set();
8936
8938
  const points = [];
8937
- for (const traceId of label.mspConnectionPairIds ?? []) {
8939
+ const connectedTraceIds = new Set(label.mspConnectionPairIds ?? []);
8940
+ if (isYOrientation(orientation)) {
8941
+ const connectedPinIds = /* @__PURE__ */ new Set();
8942
+ for (const traceId of connectedTraceIds) {
8943
+ for (const pinId of this.traceMap[traceId]?.pinIds ?? []) {
8944
+ connectedPinIds.add(pinId);
8945
+ }
8946
+ }
8947
+ let foundConnectedTrace = true;
8948
+ while (foundConnectedTrace) {
8949
+ foundConnectedTrace = false;
8950
+ for (const trace of Object.values(this.traceMap)) {
8951
+ if (connectedTraceIds.has(trace.mspPairId)) continue;
8952
+ if (trace.mspPairId.startsWith("available-net-orientation-")) continue;
8953
+ if (trace.globalConnNetId !== label.globalConnNetId) continue;
8954
+ if (!trace.pinIds.some((pinId) => connectedPinIds.has(pinId)))
8955
+ continue;
8956
+ const connectedTraces = [...connectedTraceIds].flatMap((traceId) => {
8957
+ const connectedTrace = this.traceMap[traceId];
8958
+ return connectedTrace ? [connectedTrace] : [];
8959
+ });
8960
+ if (!this.sharesVerticalRailWithAny(trace, connectedTraces)) {
8961
+ continue;
8962
+ }
8963
+ connectedTraceIds.add(trace.mspPairId);
8964
+ for (const pinId of trace.pinIds) connectedPinIds.add(pinId);
8965
+ foundConnectedTrace = true;
8966
+ }
8967
+ }
8968
+ }
8969
+ for (const traceId of connectedTraceIds) {
8938
8970
  const trace = this.traceMap[traceId];
8939
8971
  if (!trace) continue;
8940
8972
  for (const point of trace.tracePath) {
@@ -8946,6 +8978,26 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
8946
8978
  }
8947
8979
  return points;
8948
8980
  }
8981
+ sharesVerticalRailWithAny(trace, otherTraces) {
8982
+ const verticalRailXs = /* @__PURE__ */ new Set();
8983
+ for (const otherTrace of otherTraces) {
8984
+ for (let i = 0; i < otherTrace.tracePath.length - 1; i++) {
8985
+ const start = otherTrace.tracePath[i];
8986
+ const end = otherTrace.tracePath[i + 1];
8987
+ if (Math.abs(start.x - end.x) <= EPS12 && Math.abs(start.y - end.y) > EPS12) {
8988
+ verticalRailXs.add(start.x);
8989
+ }
8990
+ }
8991
+ }
8992
+ for (let i = 0; i < trace.tracePath.length - 1; i++) {
8993
+ const start = trace.tracePath[i];
8994
+ const end = trace.tracePath[i + 1];
8995
+ if (Math.abs(start.x - end.x) <= EPS12 && Math.abs(start.y - end.y) > EPS12 && [...verticalRailXs].some((x) => Math.abs(x - start.x) <= EPS12)) {
8996
+ return true;
8997
+ }
8998
+ }
8999
+ return false;
9000
+ }
8949
9001
  getAvailableOrientations(label) {
8950
9002
  const effectiveNetId = label.netId ?? label.globalConnNetId;
8951
9003
  return this.inputProblem.availableNetLabelOrientations[effectiveNetId] ?? [];
@@ -12576,6 +12628,154 @@ var getAxisAlignedSegments = (path, epsilon = 1e-6) => {
12576
12628
  return segments.sort((a, b) => b.length - a.length);
12577
12629
  };
12578
12630
 
12631
+ // lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts
12632
+ var getStubDirection = (path) => {
12633
+ const [start, end] = path;
12634
+ if (Math.abs(end.x - start.x) >= Math.abs(end.y - start.y)) {
12635
+ return end.x >= start.x ? "x+" : "x-";
12636
+ }
12637
+ return end.y >= start.y ? "y+" : "y-";
12638
+ };
12639
+ var getStubLength = (path) => {
12640
+ const [start, end] = path;
12641
+ return Math.abs(end.x - start.x) + Math.abs(end.y - start.y);
12642
+ };
12643
+ var getLabelBounds2 = (placement) => {
12644
+ const isVertical5 = placement.axis === "y";
12645
+ const width = isVertical5 ? placement.height : placement.width;
12646
+ const height = isVertical5 ? placement.width : placement.height;
12647
+ return {
12648
+ minX: placement.center.x - width / 2,
12649
+ maxX: placement.center.x + width / 2,
12650
+ minY: placement.center.y - height / 2,
12651
+ maxY: placement.center.y + height / 2
12652
+ };
12653
+ };
12654
+ var getPathBounds = (path) => ({
12655
+ minX: Math.min(...path.map((point) => point.x)),
12656
+ maxX: Math.max(...path.map((point) => point.x)),
12657
+ minY: Math.min(...path.map((point) => point.y)),
12658
+ maxY: Math.max(...path.map((point) => point.y))
12659
+ });
12660
+ var doesPathIntersectBounds = (path, bounds) => {
12661
+ for (let index = 0; index < path.length - 1; index++) {
12662
+ const segmentBounds = getPathBounds([path[index], path[index + 1]]);
12663
+ if (boundsOverlap(segmentBounds, bounds)) return true;
12664
+ }
12665
+ return false;
12666
+ };
12667
+ var resizeStub = (placement, direction, targetLength) => {
12668
+ const [start] = placement.stubTracePath;
12669
+ 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 };
12670
+ const anchorPoint = {
12671
+ x: (start.x + end.x) / 2,
12672
+ y: (start.y + end.y) / 2
12673
+ };
12674
+ return {
12675
+ ...placement,
12676
+ stubTracePath: [start, end],
12677
+ anchorPoint,
12678
+ // Extending a short row only adds wire at its free end. Keep the label
12679
+ // itself next to the pin so terminal labels can be start/end aligned and
12680
+ // collision checks continue to describe the rendered text box.
12681
+ center: placement.center
12682
+ };
12683
+ };
12684
+ var alignPortOnlyInlineNetLabelStubs = ({
12685
+ placements,
12686
+ inputProblem,
12687
+ traces,
12688
+ netLabelPlacements
12689
+ }) => {
12690
+ const chipIdByPinId = /* @__PURE__ */ new Map();
12691
+ for (const chip of inputProblem.chips) {
12692
+ for (const pin of chip.pins) chipIdByPinId.set(pin.pinId, chip.chipId);
12693
+ }
12694
+ const groups = /* @__PURE__ */ new Map();
12695
+ for (const [placementIndex, placement] of placements.entries()) {
12696
+ if (!placement.stubTracePath || placement.pinIds.length !== 1) continue;
12697
+ const direction = getStubDirection(placement.stubTracePath);
12698
+ if (!direction) continue;
12699
+ const ownerChipId = chipIdByPinId.get(placement.pinIds[0]);
12700
+ const groupKey = `${ownerChipId ?? placement.pinIds[0]}::${direction}`;
12701
+ const group = groups.get(groupKey) ?? [];
12702
+ group.push({ placementIndex, placement, direction, ownerChipId });
12703
+ groups.set(groupKey, group);
12704
+ }
12705
+ const alignedPlacements = [...placements];
12706
+ const supersededGlobalNetIds = new Set(
12707
+ placements.map((placement) => placement.globalConnNetId)
12708
+ );
12709
+ const retainedAnchoredLabelBounds = netLabelPlacements.filter(
12710
+ (placement) => !supersededGlobalNetIds.has(placement.globalConnNetId)
12711
+ ).map(getLabelBounds2);
12712
+ for (const group of groups.values()) {
12713
+ if (group.length < 2) continue;
12714
+ const targetLength = Math.max(
12715
+ ...group.map(({ placement }) => getStubLength(placement.stubTracePath))
12716
+ );
12717
+ const proposals = group.map(
12718
+ ({ placement, direction }) => resizeStub(placement, direction, targetLength)
12719
+ );
12720
+ const groupPlacementIndices = new Set(
12721
+ group.map(({ placementIndex }) => placementIndex)
12722
+ );
12723
+ const fixedInlinePlacements = placements.filter(
12724
+ (_, placementIndex) => !groupPlacementIndices.has(placementIndex)
12725
+ );
12726
+ const hasConflict = proposals.some((proposal, proposalIndex) => {
12727
+ const labelBounds = getLabelBounds2(proposal);
12728
+ const stubPath = proposal.stubTracePath;
12729
+ const stubBounds = getPathBounds(stubPath);
12730
+ const ownerChipId = group[proposalIndex].ownerChipId;
12731
+ for (const chip of inputProblem.chips) {
12732
+ const chipBounds = {
12733
+ minX: chip.center.x - chip.width / 2,
12734
+ maxX: chip.center.x + chip.width / 2,
12735
+ minY: chip.center.y - chip.height / 2,
12736
+ maxY: chip.center.y + chip.height / 2
12737
+ };
12738
+ if (boundsOverlap(labelBounds, chipBounds)) return true;
12739
+ if (chip.chipId !== ownerChipId && boundsOverlap(stubBounds, chipBounds))
12740
+ return true;
12741
+ }
12742
+ for (const textBox of inputProblem.textBoxes ?? []) {
12743
+ const textBounds = getTextBoxBounds(textBox);
12744
+ if (boundsOverlap(labelBounds, textBounds) || boundsOverlap(stubBounds, textBounds))
12745
+ return true;
12746
+ }
12747
+ for (const trace of traces) {
12748
+ if (trace.globalConnNetId === proposal.globalConnNetId) continue;
12749
+ if (doesPathIntersectBounds(trace.tracePath, labelBounds) || doesPathIntersectBounds(trace.tracePath, stubBounds))
12750
+ return true;
12751
+ }
12752
+ if (retainedAnchoredLabelBounds.some(
12753
+ (bounds) => boundsOverlap(labelBounds, bounds) || boundsOverlap(stubBounds, bounds)
12754
+ ))
12755
+ return true;
12756
+ for (const fixedPlacement of fixedInlinePlacements) {
12757
+ const fixedLabelBounds = getLabelBounds2(fixedPlacement);
12758
+ if (boundsOverlap(labelBounds, fixedLabelBounds) || boundsOverlap(stubBounds, fixedLabelBounds))
12759
+ return true;
12760
+ if (fixedPlacement.stubTracePath && (doesPathIntersectBounds(fixedPlacement.stubTracePath, labelBounds) || doesPathIntersectBounds(stubPath, fixedLabelBounds)))
12761
+ return true;
12762
+ }
12763
+ for (const [otherIndex, otherProposal] of proposals.entries()) {
12764
+ if (otherIndex === proposalIndex) continue;
12765
+ const otherLabelBounds = getLabelBounds2(otherProposal);
12766
+ if (boundsOverlap(labelBounds, otherLabelBounds)) return true;
12767
+ if (doesPathIntersectBounds(stubPath, otherLabelBounds)) return true;
12768
+ }
12769
+ return false;
12770
+ });
12771
+ if (hasConflict) continue;
12772
+ for (const [groupIndex, { placementIndex }] of group.entries()) {
12773
+ alignedPlacements[placementIndex] = proposals[groupIndex];
12774
+ }
12775
+ }
12776
+ return alignedPlacements;
12777
+ };
12778
+
12579
12779
  // lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts
12580
12780
  var DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
12581
12781
  var INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
@@ -12588,7 +12788,10 @@ var InlineNetLabelSolver = class extends BaseSolver {
12588
12788
  inlineNetLabelPlacements = [];
12589
12789
  /** Direct connections that opted in, still waiting to be processed */
12590
12790
  queuedDirectConnections;
12791
+ /** Single-pin net connections that opted in, still waiting to be processed */
12792
+ queuedPortOnlyNetConnections;
12591
12793
  tracesByPinPairKey;
12794
+ hasAlignedPortOnlyStubs = false;
12592
12795
  constructor(input) {
12593
12796
  super();
12594
12797
  this.inputProblem = input.inputProblem;
@@ -12597,6 +12800,9 @@ var InlineNetLabelSolver = class extends BaseSolver {
12597
12800
  this.queuedDirectConnections = this.inputProblem.directConnections.filter(
12598
12801
  (dc) => dc.allowInlineNetLabel && dc.netId
12599
12802
  );
12803
+ this.queuedPortOnlyNetConnections = this.inputProblem.netConnections.filter(
12804
+ (nc) => nc.allowInlineNetLabel && nc.pinIds.length === 1 && nc.netId
12805
+ );
12600
12806
  this.tracesByPinPairKey = /* @__PURE__ */ new Map();
12601
12807
  for (const trace of this.traces) {
12602
12808
  const key = getPinPairKey2(trace.pins.map((p) => p.pinId));
@@ -12619,15 +12825,97 @@ var InlineNetLabelSolver = class extends BaseSolver {
12619
12825
  }
12620
12826
  _step() {
12621
12827
  const directConnection = this.queuedDirectConnections.shift();
12622
- if (!directConnection) {
12623
- this.solved = true;
12624
- this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length;
12828
+ if (directConnection) {
12829
+ const placement = this.computeInlinePlacement(directConnection);
12830
+ if (placement) {
12831
+ this.inlineNetLabelPlacements.push(placement);
12832
+ }
12833
+ return;
12834
+ }
12835
+ const portOnlyNetConnection = this.queuedPortOnlyNetConnections.shift();
12836
+ if (portOnlyNetConnection) {
12837
+ const placement = this.computePortOnlyInlinePlacement(
12838
+ portOnlyNetConnection
12839
+ );
12840
+ if (placement) {
12841
+ this.inlineNetLabelPlacements.push(placement);
12842
+ }
12843
+ return;
12844
+ }
12845
+ if (!this.hasAlignedPortOnlyStubs) {
12846
+ this.inlineNetLabelPlacements = alignPortOnlyInlineNetLabelStubs({
12847
+ placements: this.inlineNetLabelPlacements,
12848
+ inputProblem: this.inputProblem,
12849
+ traces: this.traces,
12850
+ netLabelPlacements: this.inputNetLabelPlacements
12851
+ });
12852
+ this.hasAlignedPortOnlyStubs = true;
12625
12853
  return;
12626
12854
  }
12627
- const placement = this.computeInlinePlacement(directConnection);
12628
- if (placement) {
12629
- this.inlineNetLabelPlacements.push(placement);
12855
+ this.solved = true;
12856
+ this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length;
12857
+ }
12858
+ /**
12859
+ * Converts a conventional port-only anchored placement into an inline label
12860
+ * on a generated outward stub. The stub follows the pin's true facing
12861
+ * direction; an anchored label may finish in another direction after an
12862
+ * elbow, which is not the direction a terminal stub should leave the pin.
12863
+ */
12864
+ computePortOnlyInlinePlacement(netConnection) {
12865
+ const [pinId] = netConnection.pinIds;
12866
+ if (!pinId) return null;
12867
+ const anchoredPlacement = this.inputNetLabelPlacements.find(
12868
+ (placement) => placement.netId === netConnection.netId && placement.pinIds.length === 1 && placement.pinIds[0] === pinId
12869
+ );
12870
+ if (!anchoredPlacement) return null;
12871
+ const inputChip = this.inputProblem.chips.find(
12872
+ (chip) => chip.pins.some((pin) => pin.pinId === pinId)
12873
+ );
12874
+ const inputPin = inputChip?.pins.find((pin) => pin.pinId === pinId);
12875
+ const height = netConnection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
12876
+ const width = netConnection.inlineNetLabelWidth ?? netConnection.netLabelWidth ?? estimateInlineNetLabelWidth(netConnection.netId, height);
12877
+ const stubLength = Math.max(width + 0.2, 0.6);
12878
+ const start = inputPin ? { x: inputPin.x, y: inputPin.y } : anchoredPlacement.anchorPoint;
12879
+ const direction = inputPin && inputChip ? inputPin._facingDirection ?? getPinDirection(inputPin, inputChip) : anchoredPlacement.orientation;
12880
+ 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 };
12881
+ const axis = direction === "x+" || direction === "x-" ? "x" : "y";
12882
+ const side = axis === "x" ? "y+" : "x-";
12883
+ const anchorPoint = {
12884
+ x: (start.x + end.x) / 2,
12885
+ y: (start.y + end.y) / 2
12886
+ };
12887
+ const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN;
12888
+ const center = side === "y+" ? { x: anchorPoint.x, y: anchorPoint.y + offset } : { x: anchorPoint.x - offset, y: anchorPoint.y };
12889
+ const halfAlong = width / 2;
12890
+ const halfAcross = height / 2;
12891
+ const bounds = axis === "x" ? {
12892
+ minX: center.x - halfAlong,
12893
+ maxX: center.x + halfAlong,
12894
+ minY: center.y - halfAcross,
12895
+ maxY: center.y + halfAcross
12896
+ } : {
12897
+ minX: center.x - halfAcross,
12898
+ maxX: center.x + halfAcross,
12899
+ minY: center.y - halfAlong,
12900
+ maxY: center.y + halfAlong
12901
+ };
12902
+ if (this.isObstructed(bounds, {
12903
+ ownGlobalConnNetId: anchoredPlacement.globalConnNetId
12904
+ })) {
12905
+ return null;
12630
12906
  }
12907
+ return {
12908
+ globalConnNetId: anchoredPlacement.globalConnNetId,
12909
+ netId: netConnection.netId,
12910
+ pinIds: [pinId],
12911
+ stubTracePath: [start, end],
12912
+ axis,
12913
+ anchorPoint,
12914
+ center,
12915
+ width,
12916
+ height,
12917
+ side
12918
+ };
12631
12919
  }
12632
12920
  computeInlinePlacement(directConnection) {
12633
12921
  const traces = this.tracesByPinPairKey.get(getPinPairKey2(directConnection.pinIds)) ?? [];
@@ -12657,7 +12945,11 @@ var InlineNetLabelSolver = class extends BaseSolver {
12657
12945
  minY: center.y - halfAlong,
12658
12946
  maxY: center.y + halfAlong
12659
12947
  };
12660
- if (this.isObstructed(bounds, trace)) continue;
12948
+ if (this.isObstructed(bounds, {
12949
+ ownTrace: trace,
12950
+ ownGlobalConnNetId: trace.globalConnNetId
12951
+ }))
12952
+ continue;
12661
12953
  return {
12662
12954
  globalConnNetId: trace.globalConnNetId,
12663
12955
  netId: directConnection.netId,
@@ -12742,7 +13034,11 @@ var InlineNetLabelSolver = class extends BaseSolver {
12742
13034
  minY: center.y - halfAlong,
12743
13035
  maxY: center.y + halfAlong
12744
13036
  };
12745
- if (this.isObstructed(bounds, trace)) continue;
13037
+ if (this.isObstructed(bounds, {
13038
+ ownTrace: trace,
13039
+ ownGlobalConnNetId: trace.globalConnNetId
13040
+ }))
13041
+ continue;
12746
13042
  const anchorPoint = axis === "x" ? { x: along, y: side === "y+" ? maxY : minY } : { x: side === "x-" ? minX : maxX, y: along };
12747
13043
  return {
12748
13044
  globalConnNetId: trace.globalConnNetId,
@@ -12764,7 +13060,10 @@ var InlineNetLabelSolver = class extends BaseSolver {
12764
13060
  * An inline label may not sit on top of a chip, a component's text, or a
12765
13061
  * trace belonging to another net.
12766
13062
  */
12767
- isObstructed(bounds, ownTrace) {
13063
+ isObstructed(bounds, {
13064
+ ownTrace,
13065
+ ownGlobalConnNetId
13066
+ }) {
12768
13067
  for (const chip of this.inputProblem.chips) {
12769
13068
  const chipBounds = {
12770
13069
  minX: chip.center.x - chip.width / 2,
@@ -12778,9 +13077,9 @@ var InlineNetLabelSolver = class extends BaseSolver {
12778
13077
  if (boundsOverlap(bounds, getTextBoxBounds(textBox))) return true;
12779
13078
  }
12780
13079
  for (const trace of this.traces) {
12781
- if (trace.mspPairId === ownTrace.mspPairId) continue;
12782
- if (trace.globalConnNetId === ownTrace.globalConnNetId) continue;
12783
- if (doesPathIntersectBounds(trace.tracePath, bounds)) return true;
13080
+ if (ownTrace && trace.mspPairId === ownTrace.mspPairId) continue;
13081
+ if (trace.globalConnNetId === ownGlobalConnNetId) continue;
13082
+ if (doesPathIntersectBounds2(trace.tracePath, bounds)) return true;
12784
13083
  }
12785
13084
  return false;
12786
13085
  }
@@ -12795,10 +13094,18 @@ var InlineNetLabelSolver = class extends BaseSolver {
12795
13094
  }
12796
13095
  return keys;
12797
13096
  }
13097
+ getOutputTraces(supersededNetLabelKeys) {
13098
+ return this.traces.filter(
13099
+ (trace) => !(supersededNetLabelKeys.has(trace.globalConnNetId) && trace.mspPairId.startsWith("available-net-orientation-"))
13100
+ );
13101
+ }
12798
13102
  getOutput() {
12799
13103
  const superseded = this.getSupersededNetLabelKeys();
12800
13104
  return {
12801
- traces: this.traces,
13105
+ // AvailableNetOrientationSolver may have routed an elbow from a port to
13106
+ // the anchored label that this inline placement supersedes. Keep the
13107
+ // actual net trace, but discard that now-orphaned label connector.
13108
+ traces: this.getOutputTraces(superseded),
12802
13109
  netLabelPlacements: this.inputNetLabelPlacements.filter(
12803
13110
  (placement) => !superseded.has(placement.globalConnNetId)
12804
13111
  ),
@@ -12811,7 +13118,8 @@ var InlineNetLabelSolver = class extends BaseSolver {
12811
13118
  graphics.rects ??= [];
12812
13119
  graphics.points ??= [];
12813
13120
  graphics.texts ??= [];
12814
- for (const trace of this.traces) {
13121
+ const output = this.getOutput();
13122
+ for (const trace of output.traces) {
12815
13123
  graphics.lines.push({
12816
13124
  points: trace.tracePath,
12817
13125
  strokeColor: "purple"
@@ -12837,6 +13145,12 @@ orientation: ${label.orientation}`
12837
13145
  });
12838
13146
  }
12839
13147
  for (const inlineLabel of this.inlineNetLabelPlacements) {
13148
+ if (inlineLabel.stubTracePath) {
13149
+ graphics.lines.push({
13150
+ points: inlineLabel.stubTracePath,
13151
+ strokeColor: "purple"
13152
+ });
13153
+ }
12840
13154
  const isHorizontal4 = inlineLabel.axis === "x";
12841
13155
  graphics.rects.push({
12842
13156
  center: inlineLabel.center,
@@ -12851,12 +13165,12 @@ orientation: ${label.orientation}`
12851
13165
  ].join("\n")
12852
13166
  });
12853
13167
  graphics.texts.push({
12854
- x: inlineLabel.center.x,
12855
- y: inlineLabel.center.y,
13168
+ 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,
13169
+ 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,
12856
13170
  text: inlineLabel.netId ?? "",
12857
13171
  color: "green",
12858
13172
  fontSize: inlineLabel.height,
12859
- anchorSide: "center",
13173
+ anchorSide: inlineLabel.stubTracePath ? inlineLabel.stubTracePath[1][inlineLabel.axis] > inlineLabel.stubTracePath[0][inlineLabel.axis] ? "center_left" : "center_right" : "center",
12860
13174
  // Vertical labels read bottom-to-top, alongside the wire they name.
12861
13175
  rotation: inlineLabel.axis === "y" ? 90 : void 0
12862
13176
  });
@@ -12891,7 +13205,7 @@ var getAnchorCandidates = (segment, labelWidth, step = 0.1) => {
12891
13205
  offsets.push(slack / 2, -slack / 2);
12892
13206
  return offsets.map((offset) => pointAt(mid + offset * direction));
12893
13207
  };
12894
- var doesPathIntersectBounds = (path, bounds) => {
13208
+ var doesPathIntersectBounds2 = (path, bounds) => {
12895
13209
  for (let i = 0; i < path.length - 1; i++) {
12896
13210
  const a = path[i];
12897
13211
  const b = path[i + 1];