@tscircuit/schematic-trace-solver 0.0.148 → 0.0.150

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 (164) hide show
  1. package/.agents/skills/schematic-trace-visualization/SKILL.md +7 -2
  2. package/dist/index.d.ts +31 -2
  3. package/dist/index.js +127 -46
  4. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +115 -6
  5. package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +17 -1
  6. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/SingleNetLabelPlacementSolver.ts +2 -0
  7. package/lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/solvePortOnlyPin.ts +2 -0
  8. package/lib/types/InputProblem.ts +27 -1
  9. package/package.json +8 -1
  10. package/site/examples/example51.page.tsx +4 -0
  11. package/tests/assets/example51.json +5102 -0
  12. package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +751 -5
  13. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +660 -5
  14. package/tests/bug-reports/bug-report-20260707T020342Z/__snapshots__/bug-report-20260707T020342Z.snap.svg +44 -5
  15. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +394 -5
  16. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +170 -5
  17. package/tests/bug-reports/bug-report-20260707T134722Z/__snapshots__/bug-report-20260707T134722Z.snap.svg +79 -5
  18. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +219 -5
  19. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +65 -5
  20. package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +142 -5
  21. package/tests/bug-reports/bug-report-20260707T230831Z/__snapshots__/bug-report-20260707T230831Z.snap.svg +100 -5
  22. package/tests/bug-reports/bug-report-20260708T053736Z/__snapshots__/bug-report-20260708T053736Z.snap.svg +23 -5
  23. package/tests/bug-reports/bug-report-20260708T055430Z/__snapshots__/bug-report-20260708T055430Z.snap.svg +58 -5
  24. package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +86 -5
  25. package/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg +226 -5
  26. package/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg +191 -5
  27. package/tests/bug-reports/bug-report-20260717T031704Z/__snapshots__/bug-report-20260717T031704Z.snap.svg +30 -5
  28. package/tests/bug-reports/bug-report-20260717T042845Z/__snapshots__/bug-report-20260717T042845Z.snap.svg +37 -5
  29. package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +580 -2
  30. package/tests/bug-reports/bug-report-20260724T175257Z/__snapshots__/bug-report-20260724T175257Z.snap.svg +44 -5
  31. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +107 -5
  32. package/tests/bug-reports/bug-report-20260728T225606Z/__snapshots__/bug-report-20260728T225606Z.snap.svg +114 -5
  33. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +121 -5
  34. package/tests/bug-reports/bug-report-20260731T052229Z/__snapshots__/bug-report-20260731T052229Z.snap.svg +51 -5
  35. package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +212 -5
  36. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +569 -5
  37. package/tests/bug-reports/bug-report-20260804T225912Z/__snapshots__/bug-report-20260804T225912Z.snap.svg +37 -5
  38. package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +289 -5
  39. package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +289 -5
  40. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +149 -5
  41. package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +93 -5
  42. package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +79 -5
  43. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +244 -2
  44. package/tests/examples/__snapshots__/example01.snap.svg +44 -5
  45. package/tests/examples/__snapshots__/example02.snap.svg +51 -5
  46. package/tests/examples/__snapshots__/example03.snap.svg +86 -5
  47. package/tests/examples/__snapshots__/example04.snap.svg +30 -5
  48. package/tests/examples/__snapshots__/example05.snap.svg +30 -5
  49. package/tests/examples/__snapshots__/example06.snap.svg +30 -5
  50. package/tests/examples/__snapshots__/example07.snap.svg +58 -5
  51. package/tests/examples/__snapshots__/example08.snap.svg +37 -5
  52. package/tests/examples/__snapshots__/example09.snap.svg +149 -5
  53. package/tests/examples/__snapshots__/example10.snap.svg +65 -5
  54. package/tests/examples/__snapshots__/example11.snap.svg +51 -5
  55. package/tests/examples/__snapshots__/example12.snap.svg +51 -5
  56. package/tests/examples/__snapshots__/example13.snap.svg +72 -5
  57. package/tests/examples/__snapshots__/example14.snap.svg +79 -5
  58. package/tests/examples/__snapshots__/example15.snap.svg +86 -5
  59. package/tests/examples/__snapshots__/example16.snap.svg +51 -5
  60. package/tests/examples/__snapshots__/example17.snap.svg +51 -5
  61. package/tests/examples/__snapshots__/example18.snap.svg +58 -5
  62. package/tests/examples/__snapshots__/example19.snap.svg +44 -5
  63. package/tests/examples/__snapshots__/example20.snap.svg +44 -5
  64. package/tests/examples/__snapshots__/example21.snap.svg +72 -5
  65. package/tests/examples/__snapshots__/example22.snap.svg +51 -5
  66. package/tests/examples/__snapshots__/example23.snap.svg +51 -5
  67. package/tests/examples/__snapshots__/example24.snap.svg +51 -5
  68. package/tests/examples/__snapshots__/example25.snap.svg +86 -5
  69. package/tests/examples/__snapshots__/example26.snap.svg +51 -5
  70. package/tests/examples/__snapshots__/example27.snap.svg +72 -5
  71. package/tests/examples/__snapshots__/example28.snap.svg +44 -5
  72. package/tests/examples/__snapshots__/example29.snap.svg +324 -5
  73. package/tests/examples/__snapshots__/example30.snap.svg +107 -5
  74. package/tests/examples/__snapshots__/example31.snap.svg +37 -5
  75. package/tests/examples/__snapshots__/example32.snap.svg +149 -5
  76. package/tests/examples/__snapshots__/example33.snap.svg +93 -5
  77. package/tests/examples/__snapshots__/example34.snap.svg +51 -5
  78. package/tests/examples/__snapshots__/example35.snap.svg +58 -5
  79. package/tests/examples/__snapshots__/example36.snap.svg +44 -5
  80. package/tests/examples/__snapshots__/example37.snap.svg +72 -5
  81. package/tests/examples/__snapshots__/example38.snap.svg +30 -5
  82. package/tests/examples/__snapshots__/example39.snap.svg +37 -5
  83. package/tests/examples/__snapshots__/example40.snap.svg +100 -5
  84. package/tests/examples/__snapshots__/example41.snap.svg +44 -5
  85. package/tests/examples/__snapshots__/example42.snap.svg +65 -5
  86. package/tests/examples/__snapshots__/example43.snap.svg +58 -5
  87. package/tests/examples/__snapshots__/example44.snap.svg +107 -5
  88. package/tests/examples/__snapshots__/example45.snap.svg +51 -5
  89. package/tests/examples/__snapshots__/example46.snap.svg +93 -5
  90. package/tests/examples/__snapshots__/example47.snap.svg +93 -5
  91. package/tests/examples/__snapshots__/example48.snap.svg +65 -5
  92. package/tests/examples/__snapshots__/example49.snap.svg +177 -5
  93. package/tests/examples/__snapshots__/example50.snap.svg +93 -5
  94. package/tests/examples/__snapshots__/example51.snap.svg +952 -0
  95. package/tests/examples/example51.test.ts +12 -0
  96. package/tests/fixtures/convertSolverOutputToCircuitJson.test.ts +219 -0
  97. package/tests/fixtures/convertSolverOutputToCircuitJson.ts +1058 -0
  98. package/tests/fixtures/matcher.ts +45 -8
  99. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +762 -2
  100. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +111 -2
  101. package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +167 -2
  102. package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +65 -5
  103. package/tests/repros/__snapshots__/manufacturePartNumber-text-box.snap.svg +30 -5
  104. package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +79 -5
  105. package/tests/repros/__snapshots__/repro-atmega328p-fault-pullup.snap.svg +65 -5
  106. package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +149 -5
  107. package/tests/repros/__snapshots__/repro-board-589-regulator-section.snap.svg +55 -2
  108. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +160 -2
  109. package/tests/repros/__snapshots__/repro-bq24074-battery-charger.snap.svg +135 -5
  110. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +69 -2
  111. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +86 -5
  112. package/tests/repros/__snapshots__/repro-core-555-power-cross-net-overlap.snap.svg +44 -5
  113. package/tests/repros/__snapshots__/repro-core-subcircuit-missing-ground.snap.svg +44 -5
  114. package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +260 -0
  115. package/tests/repros/__snapshots__/repro-example35-minimize-trace-crossing.snap.svg +335 -2
  116. package/tests/repros/__snapshots__/repro-ground-label-on-lower-rail-end.snap.svg +48 -2
  117. package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +149 -5
  118. package/tests/repros/__snapshots__/repro-missing-trace-netlabel.snap.svg +72 -5
  119. package/tests/repros/__snapshots__/repro-netlabel-collision-687.snap.svg +100 -5
  120. package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +51 -5
  121. package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +90 -2
  122. package/tests/repros/__snapshots__/repro-power-group-rail-labels.snap.svg +37 -5
  123. package/tests/repros/__snapshots__/repro-rectifier-trace-overlap.snap.svg +44 -5
  124. package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +237 -2
  125. package/tests/repros/__snapshots__/repro-rp2040-usb-cc2-ground-overlap.snap.svg +72 -5
  126. package/tests/repros/__snapshots__/repro-rp2040-v3v3-label-placement.snap.svg +51 -5
  127. package/tests/repros/__snapshots__/repro-rp2040-zero-crystal-fallback-netlabels.snap.svg +30 -5
  128. package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +79 -5
  129. package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +27 -2
  130. package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +1150 -5
  131. package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +51 -5
  132. package/tests/repros/__snapshots__/repro-type-c-16pin-self-connection.snap.svg +30 -5
  133. package/tests/repros/__snapshots__/repro-type-c-16pin-to-resistor.snap.svg +30 -5
  134. package/tests/repros/__snapshots__/repro-type-c-vbus-led-ground-label.snap.svg +44 -5
  135. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +685 -2
  136. package/tests/repros/__snapshots__/repro-vcc-pin1-detour.snap.svg +27 -2
  137. package/tests/repros/__snapshots__/repro129-host-custom-symbol-passives.snap.svg +51 -5
  138. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +100 -5
  139. package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +107 -5
  140. package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +51 -5
  141. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +104 -2
  142. package/tests/repros/__snapshots__/repro5-escape-padded-text-obstacles.snap.svg +30 -5
  143. package/tests/repros/__snapshots__/repro51-overlap-junction-crossing.snap.svg +65 -5
  144. package/tests/repros/__snapshots__/repro84-stacked-gnd-label-orientation.snap.svg +27 -2
  145. package/tests/repros/__snapshots__/small-variant-resistor-facing-direction.snap.svg +58 -5
  146. package/tests/repros/__snapshots__/trace-overlap-box-resistor.snap.svg +37 -5
  147. package/tests/repros/assets/repro-core-tb67s579ftg-inline-label-routing.input.json +900 -0
  148. package/tests/repros/repro-core-tb67s579ftg-inline-label-routing.test.ts +40 -0
  149. package/tests/solvers/InlineNetLabelSolver/__snapshots__/fallback-net-label-width.snap.svg +25 -7
  150. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-anchored-label-clearance.snap.svg +34 -2
  151. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-port-stub.snap.svg +23 -5
  152. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-two-pin-net.snap.svg +23 -5
  153. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label01.snap.svg +23 -5
  154. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label02.snap.svg +23 -5
  155. package/tests/solvers/InlineNetLabelSolver/__snapshots__/terminal-stub-trace-clearance.snap.svg +71 -0
  156. package/tests/solvers/InlineNetLabelSolver/fallback-net-label-width.test.ts +10 -1
  157. package/tests/solvers/InlineNetLabelSolver/terminal-stub-trace-clearance.test.ts +102 -0
  158. package/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_01-example17-d1_1-u1_1.snap.svg +23 -5
  159. package/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_repro47-short-opposing-pins.snap.svg +20 -2
  160. package/tests/solvers/TraceCleanupSolver/__snapshots__/TraceCleanupSolver.snap.svg +44 -5
  161. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +79 -5
  162. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/MergedNetLabelObstacles.snap.svg +44 -5
  163. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg +23 -5
  164. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/SingleOverlapSolver.snap.svg +30 -5
@@ -48,7 +48,8 @@ const png = await getPngBufferFromGraphicsObject(graphics, {
48
48
 
49
49
  ## Existing Repo Helpers
50
50
 
51
- - `tests/fixtures/matcher.ts`: `expect(solver).toMatchSolverSnapshot(import.meta.path)` converts `solver.visualize()` to SVG and snapshots it.
51
+ - `tests/fixtures/matcher.ts`: `expect(solver).toMatchSolverSnapshot(import.meta.path)` converts the solver's semantic output to Circuit JSON and renders it with `circuit-to-svg`. These snapshots hide the input rats nest by default.
52
+ - `tests/fixtures/convertSolverOutputToCircuitJson.ts`: builds snapshot Circuit JSON with schematic components/symbols, ports, routed traces, net labels, text, and boxes.
52
53
  - `tests/fixtures/getLastStepGraphicsObject.ts`: filters a `GraphicsObject` to its highest numbered `step`.
53
54
  - `tests/fixtures/getLastStepSvg.ts`: converts only the last step to SVG.
54
55
  - `lib/testing/PipelineStageDebugRunner.ts`: runs `SchematicTracePipelineSolver` and writes per-stage PNG/SVG/graphics JSON artifacts.
@@ -56,7 +57,8 @@ const png = await getPngBufferFromGraphicsObject(graphics, {
56
57
 
57
58
  ## Minimal SVG Tests
58
59
 
59
- Prefer focused solver SVG snapshots over broad dumps:
60
+ Prefer focused solver SVG snapshots over broad dumps. Solver snapshots use the
61
+ semantic Circuit JSON renderer by default:
60
62
 
61
63
  ```ts
62
64
  const solver = new SchematicTracePipelineSolver(inputProblem as any)
@@ -64,6 +66,9 @@ solver.solve()
64
66
  expect(solver).toMatchSolverSnapshot(import.meta.path)
65
67
  ```
66
68
 
69
+ Use `solver.visualize()` with `getSvgFromGraphicsObject` directly when a test is
70
+ specifically about internal candidates, failed geometry, or staged debug state.
71
+
67
72
  For multi-step graphics where only the final stage matters:
68
73
 
69
74
  ```ts
package/dist/index.d.ts CHANGED
@@ -75,6 +75,13 @@ interface TextBoxes {
75
75
  }
76
76
  interface InputChip {
77
77
  chipId: ChipId;
78
+ /**
79
+ * Exact directional/oriented schematic-symbol name when the caller has it.
80
+ * `center`, `width`, and `height` describe the solver obstacle and may be
81
+ * expanded to include reference/value or manufacturer-part-number text, so
82
+ * renderers should use this symbol's geometry for the component body.
83
+ */
84
+ symbolName?: string;
78
85
  center: {
79
86
  x: number;
80
87
  y: number;
@@ -87,6 +94,13 @@ interface InputChip {
87
94
  interface InputDirectConnection {
88
95
  pinIds: [PinId, PinId];
89
96
  netId?: string;
97
+ /**
98
+ * User-facing text to render for this net label. `netId` remains the stable
99
+ * connectivity identifier and may be an internal id. When this is omitted
100
+ * but a label width is provided, renderers should use a width-preserving
101
+ * placeholder instead of exposing `netId` as label text.
102
+ */
103
+ netLabelText?: string;
90
104
  netLabelWidth?: number;
91
105
  /**
92
106
  * Width of the conventional anchored label to use only when an inline label
@@ -109,7 +123,8 @@ interface InputDirectConnection {
109
123
  allowInlineNetLabel?: boolean;
110
124
  /**
111
125
  * Extent of the inline net label along the trace. Falls back to
112
- * `netLabelWidth`, then to an estimate from the netId text.
126
+ * `netLabelWidth`, then to an estimate from `netLabelText` (or `netId` for
127
+ * backwards compatibility).
113
128
  */
114
129
  inlineNetLabelWidth?: number;
115
130
  /**
@@ -121,6 +136,13 @@ interface InputDirectConnection {
121
136
  interface InputNetConnection {
122
137
  netId: string;
123
138
  pinIds: Array<PinId>;
139
+ /**
140
+ * User-facing text to render for this net label. `netId` remains the stable
141
+ * connectivity identifier and may be an internal id. When this is omitted
142
+ * but a label width is provided, renderers should use a width-preserving
143
+ * placeholder instead of exposing `netId` as label text.
144
+ */
145
+ netLabelText?: string;
124
146
  netLabelWidth?: number;
125
147
  fallbackNetLabelWidth?: number;
126
148
  netLabelHeight?: number;
@@ -425,6 +447,7 @@ declare class SingleNetLabelPlacementSolver extends BaseSolver {
425
447
  type OverlappingSameNetTraceGroup = {
426
448
  globalConnNetId: string;
427
449
  netId?: string;
450
+ netLabelText?: string;
428
451
  overlappingTraces?: SolvedTracePath;
429
452
  portOnlyPinId?: string;
430
453
  mspConnectionPairIds?: MspConnectionPairId[];
@@ -436,6 +459,8 @@ interface NetLabelPlacement {
436
459
  * Optional user-provided net identifier (if present in the input problem).
437
460
  */
438
461
  netId?: string;
462
+ /** User-facing label content, separate from the connectivity identifier. */
463
+ netLabelText?: string;
439
464
  /**
440
465
  * MSP pair ids that the label is associated with. Port-only labels use [].
441
466
  */
@@ -1186,6 +1211,8 @@ declare const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
1186
1211
  * Gap between the trace and the near edge of the inline label text.
1187
1212
  */
1188
1213
  declare const INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
1214
+ /** Clearance between a generated terminal stub and an unrelated trace. */
1215
+ declare const INLINE_NET_LABEL_STUB_TRACE_CLEARANCE = 0.05;
1189
1216
  /**
1190
1217
  * Largest perpendicular deviation a route may have and still be labeled as one
1191
1218
  * span. A route that is straight except for elbow jogs up to this size reads
@@ -1207,6 +1234,8 @@ declare const INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4;
1207
1234
  interface InlineNetLabelPlacement {
1208
1235
  globalConnNetId: string;
1209
1236
  netId?: string;
1237
+ /** User-facing label content, separate from the connectivity identifier. */
1238
+ netLabelText?: string;
1210
1239
  mspPairId?: string;
1211
1240
  pinIds: PinId[];
1212
1241
  /**
@@ -1361,4 +1390,4 @@ declare class SchematicTracePipelineSolver extends BaseSolver {
1361
1390
  preview(): GraphicsObject;
1362
1391
  }
1363
1392
 
1364
- export { type ChipId, DEFAULT_INLINE_NET_LABEL_HEIGHT, INLINE_NET_LABEL_MAX_SPAN_JOG, INLINE_NET_LABEL_TRACE_MARGIN, type InlineNetLabelPlacement, InlineNetLabelSolver, type InputChip, type InputDirectConnection, type InputNetConnection, type InputPin, type InputProblem, type NetId, type NetLabelPlacement, type PinId, SchematicTracePipelineSolver, SchematicTraceSingleLineSolver2, type SectionId, type TextBoxes, estimateInlineNetLabelWidth };
1393
+ export { type ChipId, DEFAULT_INLINE_NET_LABEL_HEIGHT, INLINE_NET_LABEL_MAX_SPAN_JOG, INLINE_NET_LABEL_STUB_TRACE_CLEARANCE, INLINE_NET_LABEL_TRACE_MARGIN, type InlineNetLabelPlacement, InlineNetLabelSolver, type InputChip, type InputDirectConnection, type InputNetConnection, type InputPin, type InputProblem, type NetId, type NetLabelPlacement, type PinId, SchematicTracePipelineSolver, SchematicTraceSingleLineSolver2, type SectionId, type TextBoxes, estimateInlineNetLabelWidth };
package/dist/index.js CHANGED
@@ -2742,6 +2742,7 @@ function solveNetLabelPlacementForPortOnlyPin(params) {
2742
2742
  globalConnNetId: overlappingSameNetTraceGroup.globalConnNetId,
2743
2743
  dcConnNetId: void 0,
2744
2744
  netId: overlappingSameNetTraceGroup.netId,
2745
+ netLabelText: overlappingSameNetTraceGroup.netLabelText,
2745
2746
  mspConnectionPairIds: [],
2746
2747
  pinIds: [pinId],
2747
2748
  orientation,
@@ -2773,6 +2774,7 @@ function solveNetLabelPlacementForPortOnlyPin(params) {
2773
2774
  globalConnNetId: overlappingSameNetTraceGroup.globalConnNetId,
2774
2775
  dcConnNetId: void 0,
2775
2776
  netId: overlappingSameNetTraceGroup.netId,
2777
+ netLabelText: overlappingSameNetTraceGroup.netLabelText,
2776
2778
  mspConnectionPairIds: [],
2777
2779
  pinIds: [pinId],
2778
2780
  orientation: fallbackOrientation,
@@ -3059,6 +3061,7 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
3059
3061
  globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
3060
3062
  dcConnNetId: curr.dcConnNetId,
3061
3063
  netId: this.overlappingSameNetTraceGroup.netId,
3064
+ netLabelText: this.overlappingSameNetTraceGroup.netLabelText,
3062
3065
  mspConnectionPairIds: [curr.mspPairId],
3063
3066
  pinIds: [curr.pins[0].pinId, curr.pins[1].pinId],
3064
3067
  orientation,
@@ -3078,6 +3081,7 @@ var SingleNetLabelPlacementSolver = class extends BaseSolver {
3078
3081
  globalConnNetId: this.overlappingSameNetTraceGroup.globalConnNetId,
3079
3082
  dcConnNetId: bestCandidate.dcConnNetId,
3080
3083
  netId: this.overlappingSameNetTraceGroup.netId,
3084
+ netLabelText: this.overlappingSameNetTraceGroup.netLabelText,
3081
3085
  mspConnectionPairIds: [bestCandidate.mspPairId],
3082
3086
  pinIds: bestCandidate.pinIds,
3083
3087
  orientation: bestCandidate.orientation,
@@ -3133,16 +3137,25 @@ var NetLabelPlacementSolver = class extends BaseSolver {
3133
3137
  }
3134
3138
  }
3135
3139
  const userNetIdByPinId = {};
3140
+ const netLabelTextByPinId = {};
3136
3141
  for (const dc of this.inputProblem.directConnections) {
3137
3142
  if (dc.netId) {
3138
3143
  const [a, b] = dc.pinIds;
3139
3144
  userNetIdByPinId[a] = dc.netId;
3140
3145
  userNetIdByPinId[b] = dc.netId;
3141
3146
  }
3147
+ const netLabelText = dc.netLabelText?.trim();
3148
+ if (netLabelText) {
3149
+ const [a, b] = dc.pinIds;
3150
+ netLabelTextByPinId[a] = netLabelText;
3151
+ netLabelTextByPinId[b] = netLabelText;
3152
+ }
3142
3153
  }
3143
3154
  for (const nc of this.inputProblem.netConnections) {
3144
3155
  for (const pid of nc.pinIds) {
3145
3156
  userNetIdByPinId[pid] = nc.netId;
3157
+ const netLabelText = nc.netLabelText?.trim();
3158
+ if (netLabelText) netLabelTextByPinId[pid] = netLabelText;
3146
3159
  }
3147
3160
  }
3148
3161
  const groups = [];
@@ -3228,6 +3241,7 @@ var NetLabelPlacementSolver = class extends BaseSolver {
3228
3241
  const group = {
3229
3242
  globalConnNetId,
3230
3243
  netId: userNetId,
3244
+ netLabelText: [...component].map((pinId) => netLabelTextByPinId[pinId]).find((text) => Boolean(text)),
3231
3245
  overlappingTraces: rep,
3232
3246
  mspConnectionPairIds
3233
3247
  };
@@ -3239,6 +3253,7 @@ var NetLabelPlacementSolver = class extends BaseSolver {
3239
3253
  groups.push({
3240
3254
  globalConnNetId,
3241
3255
  netId: userNetId,
3256
+ netLabelText: netLabelTextByPinId[p],
3242
3257
  portOnlyPinId: p
3243
3258
  });
3244
3259
  }
@@ -12678,47 +12693,6 @@ var TraceElbowTransitionSimplificationSolver = class extends BaseSolver {
12678
12693
  }
12679
12694
  };
12680
12695
 
12681
- // lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments.ts
12682
- var getAxisAlignedSegments = (path, epsilon = 1e-6) => {
12683
- const segments = [];
12684
- let runStart = null;
12685
- let runAxis = null;
12686
- let runSign = 0;
12687
- const closeRun = (runEnd) => {
12688
- if (runStart && runAxis) {
12689
- const length = runAxis === "x" ? Math.abs(runEnd.x - runStart.x) : Math.abs(runEnd.y - runStart.y);
12690
- if (length > epsilon) {
12691
- segments.push({ start: runStart, end: runEnd, axis: runAxis, length });
12692
- }
12693
- }
12694
- runStart = null;
12695
- runAxis = null;
12696
- runSign = 0;
12697
- };
12698
- for (let i = 0; i < path.length - 1; i++) {
12699
- const a = path[i];
12700
- const b = path[i + 1];
12701
- const dx = b.x - a.x;
12702
- const dy = b.y - a.y;
12703
- if (Math.abs(dx) <= epsilon && Math.abs(dy) <= epsilon) continue;
12704
- const axis = Math.abs(dy) <= epsilon ? "x" : Math.abs(dx) <= epsilon ? "y" : null;
12705
- if (!axis) {
12706
- closeRun(a);
12707
- continue;
12708
- }
12709
- const sign = Math.sign(axis === "x" ? dx : dy);
12710
- if (runStart && runAxis === axis && runSign === sign) continue;
12711
- closeRun(a);
12712
- runStart = a;
12713
- runAxis = axis;
12714
- runSign = sign;
12715
- }
12716
- if (path.length >= 2) {
12717
- closeRun(path[path.length - 1]);
12718
- }
12719
- return segments.sort((a, b) => b.length - a.length);
12720
- };
12721
-
12722
12696
  // lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts
12723
12697
  var getStubDirection = (path) => {
12724
12698
  const [start, end] = path;
@@ -12867,6 +12841,47 @@ var alignPortOnlyInlineNetLabelStubs = ({
12867
12841
  return alignedPlacements;
12868
12842
  };
12869
12843
 
12844
+ // lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments.ts
12845
+ var getAxisAlignedSegments = (path, epsilon = 1e-6) => {
12846
+ const segments = [];
12847
+ let runStart = null;
12848
+ let runAxis = null;
12849
+ let runSign = 0;
12850
+ const closeRun = (runEnd) => {
12851
+ if (runStart && runAxis) {
12852
+ const length = runAxis === "x" ? Math.abs(runEnd.x - runStart.x) : Math.abs(runEnd.y - runStart.y);
12853
+ if (length > epsilon) {
12854
+ segments.push({ start: runStart, end: runEnd, axis: runAxis, length });
12855
+ }
12856
+ }
12857
+ runStart = null;
12858
+ runAxis = null;
12859
+ runSign = 0;
12860
+ };
12861
+ for (let i = 0; i < path.length - 1; i++) {
12862
+ const a = path[i];
12863
+ const b = path[i + 1];
12864
+ const dx = b.x - a.x;
12865
+ const dy = b.y - a.y;
12866
+ if (Math.abs(dx) <= epsilon && Math.abs(dy) <= epsilon) continue;
12867
+ const axis = Math.abs(dy) <= epsilon ? "x" : Math.abs(dx) <= epsilon ? "y" : null;
12868
+ if (!axis) {
12869
+ closeRun(a);
12870
+ continue;
12871
+ }
12872
+ const sign = Math.sign(axis === "x" ? dx : dy);
12873
+ if (runStart && runAxis === axis && runSign === sign) continue;
12874
+ closeRun(a);
12875
+ runStart = a;
12876
+ runAxis = axis;
12877
+ runSign = sign;
12878
+ }
12879
+ if (path.length >= 2) {
12880
+ closeRun(path[path.length - 1]);
12881
+ }
12882
+ return segments.sort((a, b) => b.length - a.length);
12883
+ };
12884
+
12870
12885
  // lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts
12871
12886
  var LABEL_CLEARANCE2 = 0.05;
12872
12887
  var POINT_EPSILON = 1e-6;
@@ -13229,6 +13244,7 @@ var pushAnchoredNetLabelsAwayFromInlineLabels = ({
13229
13244
  // lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts
13230
13245
  var DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18;
13231
13246
  var INLINE_NET_LABEL_TRACE_MARGIN = 0.05;
13247
+ var INLINE_NET_LABEL_STUB_TRACE_CLEARANCE = 0.05;
13232
13248
  var INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4;
13233
13249
  var getPinPairKey2 = (pinIds) => [...pinIds].sort().join("::");
13234
13250
  var InlineNetLabelSolver = class extends BaseSolver {
@@ -13325,11 +13341,20 @@ var InlineNetLabelSolver = class extends BaseSolver {
13325
13341
  );
13326
13342
  const inputPin = inputChip?.pins.find((pin) => pin.pinId === pinId);
13327
13343
  const height = connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
13328
- const width = connection.inlineNetLabelWidth ?? connection.netLabelWidth ?? estimateInlineNetLabelWidth(connection.netId, height);
13344
+ const netLabelText = connection.netLabelText?.trim() || void 0;
13345
+ const width = connection.inlineNetLabelWidth ?? connection.netLabelWidth ?? estimateInlineNetLabelWidth(netLabelText ?? connection.netId, height);
13329
13346
  const stubLength = Math.max(width + 0.2, 0.6);
13330
13347
  const start = inputPin ? { x: inputPin.x, y: inputPin.y } : anchoredPlacement.anchorPoint;
13331
13348
  const direction = inputPin && inputChip ? inputPin._facingDirection ?? getPinDirection(inputPin, inputChip) : anchoredPlacement.orientation;
13332
- 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 };
13349
+ const intendedEnd = 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 };
13350
+ const end = getCollisionLimitedTerminalStubEnd({
13351
+ start,
13352
+ intendedEnd,
13353
+ minimumLength: width + 2 * INLINE_NET_LABEL_TRACE_MARGIN,
13354
+ traces: this.traces,
13355
+ ownGlobalConnNetId: anchoredPlacement.globalConnNetId
13356
+ });
13357
+ if (!end) return null;
13333
13358
  const axis = direction === "x+" || direction === "x-" ? "x" : "y";
13334
13359
  const side = axis === "x" ? "y+" : "x-";
13335
13360
  const anchorPoint = {
@@ -13359,6 +13384,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
13359
13384
  return {
13360
13385
  globalConnNetId: anchoredPlacement.globalConnNetId,
13361
13386
  netId: connection.netId,
13387
+ netLabelText,
13362
13388
  pinIds: [pinId],
13363
13389
  stubTracePath: [start, end],
13364
13390
  axis,
@@ -13376,7 +13402,8 @@ var InlineNetLabelSolver = class extends BaseSolver {
13376
13402
  const segments = getAxisAlignedSegments(trace.tracePath);
13377
13403
  if (segments.length === 0) return null;
13378
13404
  const height = connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT;
13379
- const width = connection.inlineNetLabelWidth ?? connection.netLabelWidth ?? estimateInlineNetLabelWidth(connection.netId, height);
13405
+ const netLabelText = connection.netLabelText?.trim() || void 0;
13406
+ const width = connection.inlineNetLabelWidth ?? connection.netLabelWidth ?? estimateInlineNetLabelWidth(netLabelText ?? connection.netId, height);
13380
13407
  const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN;
13381
13408
  const usableSegments = segments.filter((segment) => segment.length >= width).sort((a, b) => b.length - a.length);
13382
13409
  for (const segment of usableSegments) {
@@ -13405,6 +13432,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
13405
13432
  return {
13406
13433
  globalConnNetId: trace.globalConnNetId,
13407
13434
  netId: connection.netId,
13435
+ netLabelText,
13408
13436
  mspPairId: trace.mspPairId,
13409
13437
  pinIds: [...connection.pinIds],
13410
13438
  axis: segment.axis,
@@ -13420,6 +13448,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
13420
13448
  const spanPlacement = this.computeSpanPlacement({
13421
13449
  trace,
13422
13450
  connection,
13451
+ netLabelText,
13423
13452
  width,
13424
13453
  height,
13425
13454
  offset
@@ -13436,6 +13465,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
13436
13465
  computeSpanPlacement({
13437
13466
  trace,
13438
13467
  connection,
13468
+ netLabelText,
13439
13469
  width,
13440
13470
  height,
13441
13471
  offset
@@ -13495,6 +13525,7 @@ var InlineNetLabelSolver = class extends BaseSolver {
13495
13525
  return {
13496
13526
  globalConnNetId: trace.globalConnNetId,
13497
13527
  netId: connection.netId,
13528
+ netLabelText,
13498
13529
  mspPairId: trace.mspPairId,
13499
13530
  pinIds: [...connection.pinIds],
13500
13531
  axis,
@@ -13629,7 +13660,7 @@ orientation: ${label.orientation}`
13629
13660
  graphics.texts.push({
13630
13661
  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,
13631
13662
  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,
13632
- text: inlineLabel.netId ?? "",
13663
+ text: inlineLabel.netLabelText ?? inlineLabel.netId ?? "",
13633
13664
  color: "green",
13634
13665
  fontSize: inlineLabel.height,
13635
13666
  anchorSide: inlineLabel.stubTracePath ? inlineLabel.stubTracePath[1][inlineLabel.axis] > inlineLabel.stubTracePath[0][inlineLabel.axis] ? "center_left" : "center_right" : "center",
@@ -13641,7 +13672,7 @@ orientation: ${label.orientation}`
13641
13672
  y: inlineLabel.anchorPoint.y,
13642
13673
  color: "green",
13643
13674
  label: `inline anchor
13644
- ${inlineLabel.netId}`
13675
+ ${inlineLabel.netLabelText ?? inlineLabel.netId}`
13645
13676
  });
13646
13677
  }
13647
13678
  return graphics;
@@ -13681,6 +13712,55 @@ var doesPathIntersectBounds2 = (path, bounds) => {
13681
13712
  }
13682
13713
  return false;
13683
13714
  };
13715
+ var GEOMETRY_EPSILON = 1e-6;
13716
+ var getCollisionLimitedTerminalStubEnd = ({
13717
+ start,
13718
+ intendedEnd,
13719
+ minimumLength,
13720
+ traces,
13721
+ ownGlobalConnNetId
13722
+ }) => {
13723
+ const isHorizontal4 = Math.abs(intendedEnd.x - start.x) >= Math.abs(intendedEnd.y - start.y);
13724
+ const alongAxis = isHorizontal4 ? "x" : "y";
13725
+ const acrossAxis = isHorizontal4 ? "y" : "x";
13726
+ const direction = Math.sign(intendedEnd[alongAxis] - start[alongAxis]) || 1;
13727
+ const intendedLength = Math.abs(intendedEnd[alongAxis] - start[alongAxis]);
13728
+ let availableLength = intendedLength;
13729
+ for (const trace of traces) {
13730
+ if (trace.globalConnNetId === ownGlobalConnNetId) continue;
13731
+ for (let segmentIndex = 0; segmentIndex < trace.tracePath.length - 1; segmentIndex++) {
13732
+ const segmentStart = trace.tracePath[segmentIndex];
13733
+ const segmentEnd = trace.tracePath[segmentIndex + 1];
13734
+ const startAlong = (segmentStart[alongAxis] - start[alongAxis]) * direction;
13735
+ const endAlong = (segmentEnd[alongAxis] - start[alongAxis]) * direction;
13736
+ const startAcross = segmentStart[acrossAxis] - start[acrossAxis];
13737
+ const endAcross = segmentEnd[acrossAxis] - start[acrossAxis];
13738
+ const minAlong = Math.min(startAlong, endAlong);
13739
+ const maxAlong = Math.max(startAlong, endAlong);
13740
+ const minAcross = Math.min(startAcross, endAcross);
13741
+ const maxAcross = Math.max(startAcross, endAcross);
13742
+ let collisionDistance;
13743
+ const isCollinear = Math.abs(startAcross) <= GEOMETRY_EPSILON && Math.abs(endAcross) <= GEOMETRY_EPSILON;
13744
+ if (isCollinear && maxAlong >= -GEOMETRY_EPSILON && minAlong <= intendedLength + GEOMETRY_EPSILON) {
13745
+ collisionDistance = Math.max(0, minAlong);
13746
+ } else {
13747
+ const crossesStubAxis = minAcross <= GEOMETRY_EPSILON && maxAcross >= -GEOMETRY_EPSILON;
13748
+ const perpendicularAlong = (startAlong + endAlong) / 2;
13749
+ if (crossesStubAxis && Math.abs(startAlong - endAlong) <= GEOMETRY_EPSILON && perpendicularAlong >= -GEOMETRY_EPSILON && perpendicularAlong <= intendedLength + GEOMETRY_EPSILON) {
13750
+ collisionDistance = Math.max(0, perpendicularAlong);
13751
+ }
13752
+ }
13753
+ if (collisionDistance !== void 0) {
13754
+ availableLength = Math.min(
13755
+ availableLength,
13756
+ collisionDistance - INLINE_NET_LABEL_STUB_TRACE_CLEARANCE
13757
+ );
13758
+ }
13759
+ }
13760
+ }
13761
+ if (availableLength + GEOMETRY_EPSILON < minimumLength) return null;
13762
+ return isHorizontal4 ? { x: start.x + direction * availableLength, y: start.y } : { x: start.x, y: start.y + direction * availableLength };
13763
+ };
13684
13764
 
13685
13765
  // lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts
13686
13766
  function definePipelineStep(solverName, solverClass, getConstructorParams, opts = {}) {
@@ -14246,6 +14326,7 @@ var SchematicTracePipelineSolver = class extends BaseSolver {
14246
14326
  export {
14247
14327
  DEFAULT_INLINE_NET_LABEL_HEIGHT,
14248
14328
  INLINE_NET_LABEL_MAX_SPAN_JOG,
14329
+ INLINE_NET_LABEL_STUB_TRACE_CLEARANCE,
14249
14330
  INLINE_NET_LABEL_TRACE_MARGIN,
14250
14331
  InlineNetLabelSolver,
14251
14332
  SchematicTracePipelineSolver,
@@ -13,11 +13,11 @@ import type {
13
13
  } from "lib/types/InputProblem"
14
14
  import { getColorFromString } from "lib/utils/getColorFromString"
15
15
  import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
16
+ import { alignPortOnlyInlineNetLabelStubs } from "./alignPortOnlyInlineNetLabelStubs"
16
17
  import {
17
18
  type AxisAlignedSegment,
18
19
  getAxisAlignedSegments,
19
20
  } from "./getAxisAlignedSegments"
20
- import { alignPortOnlyInlineNetLabelStubs } from "./alignPortOnlyInlineNetLabelStubs"
21
21
  import { pushAnchoredNetLabelsAwayFromInlineLabels } from "./pushAnchoredNetLabelsAwayFromInlineLabels"
22
22
 
23
23
  export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
@@ -27,6 +27,9 @@ export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
27
27
  */
28
28
  export const INLINE_NET_LABEL_TRACE_MARGIN = 0.05
29
29
 
30
+ /** Clearance between a generated terminal stub and an unrelated trace. */
31
+ export const INLINE_NET_LABEL_STUB_TRACE_CLEARANCE = 0.05
32
+
30
33
  /**
31
34
  * Largest perpendicular deviation a route may have and still be labeled as one
32
35
  * span. A route that is straight except for elbow jogs up to this size reads
@@ -49,6 +52,8 @@ export const INLINE_NET_LABEL_MAX_SPAN_JOG = 0.4
49
52
  export interface InlineNetLabelPlacement {
50
53
  globalConnNetId: string
51
54
  netId?: string
55
+ /** User-facing label content, separate from the connectivity identifier. */
56
+ netLabelText?: string
52
57
  mspPairId?: string
53
58
  pinIds: PinId[]
54
59
 
@@ -233,10 +238,11 @@ export class InlineNetLabelSolver extends BaseSolver {
233
238
 
234
239
  const height =
235
240
  connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
241
+ const netLabelText = connection.netLabelText?.trim() || undefined
236
242
  const width =
237
243
  connection.inlineNetLabelWidth ??
238
244
  connection.netLabelWidth ??
239
- estimateInlineNetLabelWidth(connection.netId, height)
245
+ estimateInlineNetLabelWidth(netLabelText ?? connection.netId, height)
240
246
 
241
247
  // Leave a small wire tail at both ends of the text so it unmistakably
242
248
  // reads as a label on a trace rather than free-standing text.
@@ -248,7 +254,7 @@ export class InlineNetLabelSolver extends BaseSolver {
248
254
  inputPin && inputChip
249
255
  ? (inputPin._facingDirection ?? getPinDirection(inputPin, inputChip))
250
256
  : anchoredPlacement.orientation
251
- const end: Point =
257
+ const intendedEnd: Point =
252
258
  direction === "x+"
253
259
  ? { x: start.x + stubLength, y: start.y }
254
260
  : direction === "x-"
@@ -257,6 +263,15 @@ export class InlineNetLabelSolver extends BaseSolver {
257
263
  ? { x: start.x, y: start.y + stubLength }
258
264
  : { x: start.x, y: start.y - stubLength }
259
265
 
266
+ const end = getCollisionLimitedTerminalStubEnd({
267
+ start,
268
+ intendedEnd,
269
+ minimumLength: width + 2 * INLINE_NET_LABEL_TRACE_MARGIN,
270
+ traces: this.traces,
271
+ ownGlobalConnNetId: anchoredPlacement.globalConnNetId,
272
+ })
273
+ if (!end) return null
274
+
260
275
  const axis: InlineNetLabelPlacement["axis"] =
261
276
  direction === "x+" || direction === "x-" ? "x" : "y"
262
277
  const side: InlineNetLabelPlacement["side"] = axis === "x" ? "y+" : "x-"
@@ -298,6 +313,7 @@ export class InlineNetLabelSolver extends BaseSolver {
298
313
  return {
299
314
  globalConnNetId: anchoredPlacement.globalConnNetId,
300
315
  netId: connection.netId,
316
+ netLabelText,
301
317
  pinIds: [pinId],
302
318
  stubTracePath: [start, end],
303
319
  axis,
@@ -324,10 +340,11 @@ export class InlineNetLabelSolver extends BaseSolver {
324
340
 
325
341
  const height =
326
342
  connection.inlineNetLabelHeight ?? DEFAULT_INLINE_NET_LABEL_HEIGHT
343
+ const netLabelText = connection.netLabelText?.trim() || undefined
327
344
  const width =
328
345
  connection.inlineNetLabelWidth ??
329
346
  connection.netLabelWidth ??
330
- estimateInlineNetLabelWidth(connection.netId!, height)
347
+ estimateInlineNetLabelWidth(netLabelText ?? connection.netId!, height)
331
348
 
332
349
  const offset = height / 2 + INLINE_NET_LABEL_TRACE_MARGIN
333
350
 
@@ -385,6 +402,7 @@ export class InlineNetLabelSolver extends BaseSolver {
385
402
  return {
386
403
  globalConnNetId: trace.globalConnNetId,
387
404
  netId: connection.netId,
405
+ netLabelText,
388
406
  mspPairId: trace.mspPairId,
389
407
  pinIds: [...connection.pinIds],
390
408
  axis: segment.axis,
@@ -405,6 +423,7 @@ export class InlineNetLabelSolver extends BaseSolver {
405
423
  const spanPlacement = this.computeSpanPlacement({
406
424
  trace,
407
425
  connection,
426
+ netLabelText,
408
427
  width,
409
428
  height,
410
429
  offset,
@@ -425,12 +444,14 @@ export class InlineNetLabelSolver extends BaseSolver {
425
444
  private computeSpanPlacement({
426
445
  trace,
427
446
  connection,
447
+ netLabelText,
428
448
  width,
429
449
  height,
430
450
  offset,
431
451
  }: {
432
452
  trace: SolvedTracePath
433
453
  connection: InlineEligibleConnection
454
+ netLabelText?: string
434
455
  width: number
435
456
  height: number
436
457
  offset: number
@@ -521,6 +542,7 @@ export class InlineNetLabelSolver extends BaseSolver {
521
542
  return {
522
543
  globalConnNetId: trace.globalConnNetId,
523
544
  netId: connection.netId,
545
+ netLabelText,
524
546
  mspPairId: trace.mspPairId,
525
547
  pinIds: [...connection.pinIds],
526
548
  axis,
@@ -691,7 +713,7 @@ export class InlineNetLabelSolver extends BaseSolver {
691
713
  ? -inlineLabel.width / 2
692
714
  : inlineLabel.width / 2)
693
715
  : inlineLabel.center.y,
694
- text: inlineLabel.netId ?? "",
716
+ text: inlineLabel.netLabelText ?? inlineLabel.netId ?? "",
695
717
  color: "green",
696
718
  fontSize: inlineLabel.height,
697
719
  anchorSide: inlineLabel.stubTracePath
@@ -707,7 +729,7 @@ export class InlineNetLabelSolver extends BaseSolver {
707
729
  x: inlineLabel.anchorPoint.x,
708
730
  y: inlineLabel.anchorPoint.y,
709
731
  color: "green",
710
- label: `inline anchor\n${inlineLabel.netId}`,
732
+ label: `inline anchor\n${inlineLabel.netLabelText ?? inlineLabel.netId}`,
711
733
  })
712
734
  }
713
735
 
@@ -777,3 +799,90 @@ const doesPathIntersectBounds = (path: Point[], bounds: Bounds): boolean => {
777
799
  }
778
800
  return false
779
801
  }
802
+
803
+ const GEOMETRY_EPSILON = 1e-6
804
+
805
+ /**
806
+ * Shortens an axis-aligned terminal stub before the first unrelated trace.
807
+ * Coordinates are schematic-world millimetres with +x right and +y up.
808
+ */
809
+ const getCollisionLimitedTerminalStubEnd = ({
810
+ start,
811
+ intendedEnd,
812
+ minimumLength,
813
+ traces,
814
+ ownGlobalConnNetId,
815
+ }: {
816
+ start: Point
817
+ intendedEnd: Point
818
+ minimumLength: number
819
+ traces: SolvedTracePath[]
820
+ ownGlobalConnNetId: string
821
+ }): Point | null => {
822
+ const isHorizontal =
823
+ Math.abs(intendedEnd.x - start.x) >= Math.abs(intendedEnd.y - start.y)
824
+ const alongAxis = isHorizontal ? "x" : "y"
825
+ const acrossAxis = isHorizontal ? "y" : "x"
826
+ const direction = Math.sign(intendedEnd[alongAxis] - start[alongAxis]) || 1
827
+ const intendedLength = Math.abs(intendedEnd[alongAxis] - start[alongAxis])
828
+ let availableLength = intendedLength
829
+
830
+ for (const trace of traces) {
831
+ if (trace.globalConnNetId === ownGlobalConnNetId) continue
832
+
833
+ for (
834
+ let segmentIndex = 0;
835
+ segmentIndex < trace.tracePath.length - 1;
836
+ segmentIndex++
837
+ ) {
838
+ const segmentStart = trace.tracePath[segmentIndex]!
839
+ const segmentEnd = trace.tracePath[segmentIndex + 1]!
840
+ const startAlong =
841
+ (segmentStart[alongAxis] - start[alongAxis]) * direction
842
+ const endAlong = (segmentEnd[alongAxis] - start[alongAxis]) * direction
843
+ const startAcross = segmentStart[acrossAxis] - start[acrossAxis]
844
+ const endAcross = segmentEnd[acrossAxis] - start[acrossAxis]
845
+ const minAlong = Math.min(startAlong, endAlong)
846
+ const maxAlong = Math.max(startAlong, endAlong)
847
+ const minAcross = Math.min(startAcross, endAcross)
848
+ const maxAcross = Math.max(startAcross, endAcross)
849
+
850
+ let collisionDistance: number | undefined
851
+ const isCollinear =
852
+ Math.abs(startAcross) <= GEOMETRY_EPSILON &&
853
+ Math.abs(endAcross) <= GEOMETRY_EPSILON
854
+ if (
855
+ isCollinear &&
856
+ maxAlong >= -GEOMETRY_EPSILON &&
857
+ minAlong <= intendedLength + GEOMETRY_EPSILON
858
+ ) {
859
+ collisionDistance = Math.max(0, minAlong)
860
+ } else {
861
+ const crossesStubAxis =
862
+ minAcross <= GEOMETRY_EPSILON && maxAcross >= -GEOMETRY_EPSILON
863
+ const perpendicularAlong = (startAlong + endAlong) / 2
864
+ if (
865
+ crossesStubAxis &&
866
+ Math.abs(startAlong - endAlong) <= GEOMETRY_EPSILON &&
867
+ perpendicularAlong >= -GEOMETRY_EPSILON &&
868
+ perpendicularAlong <= intendedLength + GEOMETRY_EPSILON
869
+ ) {
870
+ collisionDistance = Math.max(0, perpendicularAlong)
871
+ }
872
+ }
873
+
874
+ if (collisionDistance !== undefined) {
875
+ availableLength = Math.min(
876
+ availableLength,
877
+ collisionDistance - INLINE_NET_LABEL_STUB_TRACE_CLEARANCE,
878
+ )
879
+ }
880
+ }
881
+ }
882
+
883
+ if (availableLength + GEOMETRY_EPSILON < minimumLength) return null
884
+
885
+ return isHorizontal
886
+ ? { x: start.x + direction * availableLength, y: start.y }
887
+ : { x: start.x, y: start.y + direction * availableLength }
888
+ }