@tscircuit/schematic-trace-solver 0.0.186 → 0.0.188

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 (53) hide show
  1. package/dist/index.d.ts +40 -23
  2. package/dist/index.js +1041 -319
  3. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +611 -230
  4. package/lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts +2 -6
  5. package/lib/solvers/InlineNetLabelSolver/getInlineLabelObstacles.ts +64 -0
  6. package/lib/solvers/InlineNetLabelSolver/getLocalTraceLabelShifts.ts +407 -0
  7. package/lib/solvers/InlineNetLabelSolver/getOutputLabelCollisions.ts +145 -0
  8. package/lib/solvers/InlineNetLabelSolver/isLabelRepresentedInline.ts +15 -0
  9. package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +61 -31
  10. package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +83 -14
  11. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +4 -3
  12. package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +14 -11
  13. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +3 -0
  14. package/lib/types/InputProblem.ts +2 -2
  15. package/package.json +1 -1
  16. package/site/bug-reports/bug-report-20260907T083336Z.page.tsx +4 -0
  17. package/site/bug-reports/bug-report-20260907T110144Z.page.tsx +4 -0
  18. package/site/bug-reports/bug-report-20260907T145640Z.page.tsx +4 -0
  19. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +2 -2
  20. package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +18 -18
  21. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +2 -2
  22. package/tests/bug-reports/bug-report-20260819T091818Z/bug-report-20260819T091818Z.test.ts +21 -0
  23. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +5 -38
  24. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +3 -9
  25. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +18 -30
  26. package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.test.ts +33 -0
  27. package/tests/bug-reports/bug-report-20260907T083336Z/__snapshots__/bug-report-20260907T083336Z.snap.svg +520 -0
  28. package/tests/bug-reports/bug-report-20260907T083336Z/bug-report-20260907T083336Z.json +2031 -0
  29. package/tests/bug-reports/bug-report-20260907T083336Z/bug-report-20260907T083336Z.test.ts +27 -0
  30. package/tests/bug-reports/bug-report-20260907T110144Z/__snapshots__/bug-report-20260907T110144Z.snap.svg +282 -0
  31. package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.json +1073 -0
  32. package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.test.ts +12 -0
  33. package/tests/bug-reports/bug-report-20260907T145640Z/__snapshots__/bug-report-20260907T145640Z.snap.svg +1107 -0
  34. package/tests/bug-reports/bug-report-20260907T145640Z/bug-report-20260907T145640Z.json +3563 -0
  35. package/tests/bug-reports/bug-report-20260907T145640Z/bug-report-20260907T145640Z.test.ts +117 -0
  36. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +2 -2
  37. package/tests/repros/__snapshots__/repro-battery-management-bq24073.snap.svg +4 -4
  38. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +2 -2
  39. package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +20 -20
  40. package/tests/repros/__snapshots__/repro-esp12f-section.snap.svg +37 -55
  41. package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +44 -68
  42. package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +3 -9
  43. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +50 -74
  44. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +2 -2
  45. package/tests/repros/repro-battery-management-bq24073.test.ts +13 -1
  46. package/tests/repros/repro-hub-usb-sheet.test.ts +25 -0
  47. package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +59 -13
  48. package/tests/solvers/InlineNetLabelSolver/local-trace-label-shifts.test.ts +296 -0
  49. package/tests/solvers/InlineNetLabelSolver/multi-pin-connected-components.test.ts +7 -4
  50. package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +2 -1
  51. package/tests/solvers/InlineNetLabelSolver/shared-direct-and-named-connection.test.ts +62 -0
  52. package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +9 -3
  53. package/tests/solvers/SameNetJunctionAlignmentSolver/parallel-load-rails.test.ts +24 -0
@@ -0,0 +1,296 @@
1
+ import { expect, test } from "bun:test"
2
+ import type { Point } from "@tscircuit/math-utils"
3
+ import type { InputProblem } from "lib/types/InputProblem"
4
+ import {
5
+ InlineNetLabelSolver,
6
+ type InlineNetLabelOutput,
7
+ } from "lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver"
8
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
9
+ import { getLocalTraceLabelShifts } from "lib/solvers/InlineNetLabelSolver/getLocalTraceLabelShifts"
10
+ import {
11
+ getOutputLabelCollisions,
12
+ sameOutputLabelCollision,
13
+ } from "lib/solvers/InlineNetLabelSolver/getOutputLabelCollisions"
14
+
15
+ const trace = (net: string, path: Point[]): SolvedTracePath => ({
16
+ mspPairId: net,
17
+ mspConnectionPairIds: [net],
18
+ globalConnNetId: net,
19
+ dcConnNetId: net,
20
+ pinIds: [`${net}.1`, `${net}.2`],
21
+ pins: [
22
+ { pinId: `${net}.1`, chipId: "A", ...path[0]! },
23
+ { pinId: `${net}.2`, chipId: "B", ...path.at(-1)! },
24
+ ],
25
+ tracePath: path,
26
+ })
27
+ const fixture = () => {
28
+ const input: InputProblem = {
29
+ chips: [],
30
+ netConnections: [],
31
+ directConnections: [],
32
+ availableNetLabelOrientations: {},
33
+ }
34
+ const output: Omit<InlineNetLabelOutput, "inputProblem"> = {
35
+ traces: [
36
+ trace("wire", [
37
+ { x: 0, y: 1 },
38
+ { x: 1, y: 1 },
39
+ { x: 1, y: -1 },
40
+ { x: 0, y: -1 },
41
+ ]),
42
+ ],
43
+ netLabelPlacements: [
44
+ {
45
+ globalConnNetId: "label",
46
+ netId: "label",
47
+ pinIds: ["label.1"],
48
+ mspConnectionPairIds: [],
49
+ orientation: "x+",
50
+ anchorPoint: { x: 0.8, y: 0 },
51
+ center: { x: 1, y: 0 },
52
+ width: 0.4,
53
+ height: 0.2,
54
+ },
55
+ ],
56
+ inlineNetLabelPlacements: [],
57
+ }
58
+ return { input, output }
59
+ }
60
+
61
+ for (const rotation of [0, 1, 2, 3])
62
+ test(`slides an existing leg without adding bends (rotation ${rotation})`, () => {
63
+ const { input, output } = fixture()
64
+ const rotate = (p: Point): Point => {
65
+ for (let i = 0; i < rotation; i++) p = { x: -p.y, y: p.x }
66
+ return p
67
+ }
68
+ output.traces[0]!.tracePath = output.traces[0]!.tracePath.map(rotate)
69
+ output.traces[0]!.pins = output.traces[0]!.pins.map((pin) => ({
70
+ ...pin,
71
+ ...rotate(pin),
72
+ })) as SolvedTracePath["pins"]
73
+ const label = output.netLabelPlacements[0]!
74
+ label.anchorPoint = rotate(label.anchorPoint)
75
+ label.center = rotate(label.center)
76
+ label.orientation = (["x+", "y+", "x-", "y-"] as const)[rotation]!
77
+ if (rotation % 2) [label.width, label.height] = [label.height, label.width]
78
+ const saved = structuredClone(output)
79
+ const proposals = [...getLocalTraceLabelShifts(input, output)]
80
+ expect(proposals.length).toBeGreaterThan(0)
81
+ for (const proposal of proposals) {
82
+ const path = proposal.traces[0]!.tracePath
83
+ expect(path).toHaveLength(4)
84
+ expect(path[0]).toEqual(output.traces[0]!.tracePath[0])
85
+ expect(path.at(-1)).toEqual(output.traces[0]!.tracePath.at(-1))
86
+ expect([...getOutputLabelCollisions(proposal)]).toEqual([])
87
+ }
88
+ expect(output).toEqual(saved)
89
+ })
90
+
91
+ test.each([
92
+ "chip",
93
+ "parallel wire",
94
+ "new crossing",
95
+ "junction",
96
+ "unconnected pin",
97
+ ] as const)(
98
+ "does not force a detour through a blocked corridor: %s",
99
+ (obstacle) => {
100
+ const { input, output } = fixture()
101
+ if (obstacle === "chip")
102
+ input.chips.push(
103
+ ...[0.75, 1.25].map((x) => ({
104
+ chipId: `C${x}`,
105
+ center: { x, y: 0 },
106
+ width: 0.08,
107
+ height: 0.5,
108
+ pins: [],
109
+ })),
110
+ )
111
+ if (obstacle === "parallel wire")
112
+ output.traces.push(
113
+ ...[0.75, 1.25].map((x) =>
114
+ trace(`other${x}`, [
115
+ { x, y: -2 },
116
+ { x, y: 2 },
117
+ ]),
118
+ ),
119
+ )
120
+ if (obstacle === "new crossing")
121
+ output.traces.push(
122
+ trace("other", [
123
+ { x: 0.6, y: 0 },
124
+ { x: 0.9, y: 0 },
125
+ ]),
126
+ trace("other2", [
127
+ { x: 1.1, y: 0 },
128
+ { x: 1.4, y: 0 },
129
+ ]),
130
+ )
131
+ if (obstacle === "junction")
132
+ output.traces.push({
133
+ ...trace("branch", [
134
+ { x: 1, y: 0.5 },
135
+ { x: 2, y: 0.5 },
136
+ ]),
137
+ globalConnNetId: "wire",
138
+ })
139
+ if (obstacle === "unconnected pin")
140
+ input.chips.push({
141
+ chipId: "ports",
142
+ center: { x: 10, y: 10 },
143
+ width: 1,
144
+ height: 1,
145
+ pins: [0.79, 1.21].map((x) => ({ pinId: `NC${x}`, x, y: 0 })),
146
+ })
147
+ expect([...getLocalTraceLabelShifts(input, output)]).toEqual([])
148
+ },
149
+ )
150
+
151
+ test("moves a neighboring power label with its short connector to free a side corridor", () => {
152
+ const { input, output } = fixture()
153
+ input.chips.push({
154
+ chipId: "C",
155
+ center: { x: 0.75, y: 0 },
156
+ width: 0.08,
157
+ height: 0.5,
158
+ pins: [],
159
+ })
160
+ const power = {
161
+ ...trace("power", [
162
+ { x: 0, y: -0.7 },
163
+ { x: 1.4, y: -0.7 },
164
+ { x: 1.4, y: 0.8 },
165
+ ]),
166
+ pinIds: ["power.1"],
167
+ }
168
+ power.pins = [power.pins[0], power.pins[0]]
169
+ output.traces.push(power)
170
+ output.netLabelPlacements.push({
171
+ globalConnNetId: "power",
172
+ netId: "power",
173
+ pinIds: ["power.1"],
174
+ mspConnectionPairIds: [],
175
+ orientation: "y+",
176
+ anchorPoint: { x: 1.4, y: 0.8 },
177
+ center: { x: 1.4, y: 0.95 },
178
+ width: 0.6,
179
+ height: 0.3,
180
+ })
181
+ const proposals = [...getLocalTraceLabelShifts(input, output)]
182
+ expect(proposals.length).toBeGreaterThan(0)
183
+ for (const proposal of proposals) {
184
+ expect(proposal.traces[0]!.tracePath).toHaveLength(4)
185
+ expect(proposal.traces[1]!.tracePath).toHaveLength(3)
186
+ expect(proposal.traces[1]!.tracePath[0]).toEqual(power.tracePath[0])
187
+ expect(proposal.netLabelPlacements[1]!.anchorPoint.x).toBeGreaterThan(1.4)
188
+ expect([...getOutputLabelCollisions(proposal)]).toEqual([])
189
+ }
190
+ })
191
+
192
+ test("clears a moved rail's own tag and a neighboring power label in one proposal", () => {
193
+ const { input, output } = fixture()
194
+ input.chips.push({
195
+ chipId: "C",
196
+ center: { x: 0.75, y: 0 },
197
+ width: 0.08,
198
+ height: 0.5,
199
+ pins: [],
200
+ })
201
+ const power = {
202
+ ...trace("power", [
203
+ { x: 0, y: -0.8 },
204
+ { x: 1.125, y: -0.8 },
205
+ { x: 1.125, y: -0.6 },
206
+ ]),
207
+ pinIds: ["power.1"],
208
+ }
209
+ power.pins = [power.pins[0], power.pins[0]]
210
+ output.traces.push(power)
211
+ output.netLabelPlacements.push(
212
+ {
213
+ globalConnNetId: "wire",
214
+ netId: "wire",
215
+ pinIds: ["wire.1", "wire.2"],
216
+ mspConnectionPairIds: [],
217
+ orientation: "x-",
218
+ anchorPoint: { x: 1, y: -0.5 },
219
+ center: { x: 0.8, y: -0.5 },
220
+ width: 0.4,
221
+ height: 0.2,
222
+ },
223
+ {
224
+ globalConnNetId: "power",
225
+ netId: "power",
226
+ pinIds: ["power.1"],
227
+ mspConnectionPairIds: [],
228
+ orientation: "y+",
229
+ anchorPoint: { x: 1.125, y: -0.6 },
230
+ center: { x: 1.125, y: -0.5 },
231
+ width: 0.15,
232
+ height: 0.2,
233
+ },
234
+ )
235
+ const saved = structuredClone(output)
236
+ const proposals = [...getLocalTraceLabelShifts(input, output)]
237
+ expect(proposals.length).toBeGreaterThan(0)
238
+ for (const proposal of proposals) {
239
+ expect(proposal.traces.map((trace) => trace.tracePath.length)).toEqual([
240
+ 4, 3,
241
+ ])
242
+ expect(proposal.netLabelPlacements[1]!.anchorPoint.x).toBeCloseTo(1.25)
243
+ expect(proposal.netLabelPlacements[2]!.anchorPoint.x).toBeGreaterThan(1.25)
244
+ expect([...getOutputLabelCollisions(proposal)]).toEqual([])
245
+ }
246
+ expect(output).toEqual(saved)
247
+ })
248
+
249
+ test("collision identity preserves opaque trace, net, and pin IDs", () => {
250
+ const first = fixture().output
251
+ first.traces[0]!.mspPairId = "a/b"
252
+ first.netLabelPlacements[0]!.globalConnNetId = "c"
253
+ first.netLabelPlacements[0]!.pinIds = ["p,q"]
254
+ const second = structuredClone(first)
255
+ second.traces[0]!.mspPairId = "a"
256
+ second.netLabelPlacements[0]!.globalConnNetId = "b/c"
257
+ const a = getOutputLabelCollisions(first)[0]!
258
+ const b = getOutputLabelCollisions(second)[0]!
259
+ expect(a).toMatchObject({
260
+ kind: "trace-label",
261
+ trace: { kind: "routed", id: "a/b" },
262
+ })
263
+ expect(sameOutputLabelCollision(a, b)).toBe(false)
264
+ const splitPins = structuredClone(first)
265
+ splitPins.netLabelPlacements[0]!.pinIds = ["p", "q"]
266
+ expect(
267
+ sameOutputLabelCollision(a, getOutputLabelCollisions(splitPins)[0]!),
268
+ ).toBe(false)
269
+ expect(sameOutputLabelCollision(a, structuredClone(a))).toBe(true)
270
+ })
271
+
272
+ test("refinement steps candidates and retains output when they exhaust their budget", () => {
273
+ const { input, output } = fixture()
274
+ const solver = new InlineNetLabelSolver({
275
+ inputProblem: input,
276
+ ...output,
277
+ resolveTraceCollisions: true,
278
+ })
279
+ let failedCandidates = 0
280
+ while (!solver.solved && !solver.failed) {
281
+ if (solver.activeSubSolver) {
282
+ const candidate = solver.activeSubSolver
283
+ expect(candidate.iterations).toBe(0)
284
+ candidate.MAX_ITERATIONS = 0
285
+ solver.step()
286
+ expect(candidate.failed).toBe(true)
287
+ failedCandidates++
288
+ } else solver.step()
289
+ }
290
+ expect(failedCandidates).toBeGreaterThan(0)
291
+ expect(solver.solved).toBe(true)
292
+ expect(solver.getOutput().traces).toEqual(output.traces)
293
+ expect(solver.getOutput().netLabelPlacements).toEqual(
294
+ output.netLabelPlacements,
295
+ )
296
+ })
@@ -175,9 +175,11 @@ test("a short routed component is retained when replacing it would strand an int
175
175
  solver.solve()
176
176
 
177
177
  const output = solver.getOutput()
178
- expect(output.inlineNetLabelPlacements).toHaveLength(0)
178
+ expect(output.inlineNetLabelPlacements.map((label) => label.pinIds)).toEqual([
179
+ ["U1.1"],
180
+ ])
179
181
  expect(output.traces).toEqual([shortTrace])
180
- expect(output.netLabelPlacements).toEqual(anchoredLabels)
182
+ expect(output.netLabelPlacements).toEqual([anchoredLabels[0]!])
181
183
  })
182
184
 
183
185
  test("a generated anchored-label connector becomes an inline terminal stub", () => {
@@ -218,8 +220,8 @@ test("a generated anchored-label connector becomes an inline terminal stub", ()
218
220
  availableNetLabelOrientations: { SIGNAL: ["x-"] },
219
221
  }
220
222
  const generatedLabelConnector: SolvedTracePath = {
221
- mspPairId: "available-net-orientation-0-SIGNAL",
222
- mspConnectionPairIds: ["available-net-orientation-0-SIGNAL"],
223
+ mspPairId: "opaque/connector:1",
224
+ mspConnectionPairIds: ["opaque/connector:1"],
223
225
  dcConnNetId: "SIGNAL",
224
226
  globalConnNetId: "SIGNAL",
225
227
  userNetId: "SIGNAL",
@@ -237,6 +239,7 @@ test("a generated anchored-label connector becomes an inline terminal stub", ()
237
239
  const solver = new InlineNetLabelSolver({
238
240
  inputProblem,
239
241
  traces: [generatedLabelConnector],
242
+ netLabelConnectorTraceIds: new Set([generatedLabelConnector.mspPairId]),
240
243
  netLabelPlacements: [],
241
244
  })
242
245
  solver.solve()
@@ -76,6 +76,7 @@ test("pushes a regular endpoint label and its wick past nearby inline text", ()
76
76
  traces,
77
77
  netLabelPlacements,
78
78
  inlineNetLabelPlacements,
79
+ netLabelConnectorTraceIds: new Set(traces.map((trace) => trace.mspPairId)),
79
80
  })
80
81
 
81
82
  expect(output.movedLabelCount).toBe(1)
@@ -289,7 +290,7 @@ test("moves a contiguous label row together and shoves an anchored obstacle", ()
289
290
  expect(output.netLabelPlacements[2]!.center.x).toBeLessThan(-2.5)
290
291
  expect(
291
292
  output.traces.filter((trace) =>
292
- trace.mspPairId.startsWith("inline-net-label-clearance-"),
293
+ output.netLabelConnectorTraceIds.has(trace.mspPairId),
293
294
  ),
294
295
  ).toHaveLength(3)
295
296
  })
@@ -0,0 +1,62 @@
1
+ import { expect, test } from "bun:test"
2
+ import { InlineNetLabelSolver } from "lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver"
3
+ import type { InputProblem } from "lib/types/InputProblem"
4
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
5
+
6
+ test.each(["CLOCK", ".A > .B"])(
7
+ "a named net and its direct pair %s produce one inline label",
8
+ (directName) => {
9
+ const connection = {
10
+ netId: "CLOCK",
11
+ pinIds: ["A.1", "B.1"] as [string, string],
12
+ allowInlineNetLabel: true,
13
+ inlineNetLabelWidth: 0.5,
14
+ inlineNetLabelHeight: 0.12,
15
+ }
16
+ const input: InputProblem = {
17
+ chips: [
18
+ {
19
+ chipId: "A",
20
+ center: { x: 0, y: 0 },
21
+ width: 1,
22
+ height: 1,
23
+ pins: [{ pinId: "A.1", x: 0.5, y: 0 }],
24
+ },
25
+ {
26
+ chipId: "B",
27
+ center: { x: 3, y: 0 },
28
+ width: 1,
29
+ height: 1,
30
+ pins: [{ pinId: "B.1", x: 2.5, y: 0 }],
31
+ },
32
+ ],
33
+ directConnections: [{ ...connection, netId: directName }],
34
+ netConnections: [connection],
35
+ availableNetLabelOrientations: {},
36
+ }
37
+ const trace: SolvedTracePath = {
38
+ mspPairId: "pair",
39
+ mspConnectionPairIds: ["pair"],
40
+ globalConnNetId: "CLOCK",
41
+ dcConnNetId: "CLOCK",
42
+ pinIds: connection.pinIds,
43
+ pins: [
44
+ { pinId: "A.1", chipId: "A", x: 0.5, y: 0 },
45
+ { pinId: "B.1", chipId: "B", x: 2.5, y: 0 },
46
+ ],
47
+ tracePath: [
48
+ { x: 0.5, y: 0 },
49
+ { x: 2.5, y: 0 },
50
+ ],
51
+ }
52
+ const solver = new InlineNetLabelSolver({
53
+ inputProblem: input,
54
+ traces: [trace],
55
+ netLabelPlacements: [],
56
+ })
57
+ solver.solve()
58
+ expect(solver.getOutput().inlineNetLabelPlacements).toHaveLength(1)
59
+ expect(solver.getOutput().inlineNetLabelPlacements[0]!.netId).toBe("CLOCK")
60
+ expect(solver.getOutput().traces).toEqual([trace])
61
+ },
62
+ )
@@ -3,7 +3,7 @@ import { InlineNetLabelSolver } from "lib/solvers/InlineNetLabelSolver/InlineNet
3
3
  import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
4
4
  import type { InputProblem } from "lib/types/InputProblem"
5
5
 
6
- test("two-pin terminal stubs fall back atomically when one endpoint is obstructed", () => {
6
+ test("disconnected endpoints keep independent label styles when one is obstructed", () => {
7
7
  const inputProblem: InputProblem = {
8
8
  chips: [
9
9
  {
@@ -79,6 +79,12 @@ test("two-pin terminal stubs fall back atomically when one endpoint is obstructe
79
79
  })
80
80
  solver.solve()
81
81
 
82
- expect(solver.inlineNetLabelPlacements).toHaveLength(0)
83
- expect(solver.getOutput().netLabelPlacements).toHaveLength(2)
82
+ expect(solver.inlineNetLabelPlacements.map((label) => label.pinIds)).toEqual([
83
+ ["U2.1"],
84
+ ])
85
+ expect(solver.getOutput().netLabelPlacements).toEqual([
86
+ netLabelPlacements[0]!,
87
+ ])
88
+ expect(netLabelPlacements).toHaveLength(2)
89
+ expect(solver.getOutput().traces).toHaveLength(0)
84
90
  })
@@ -290,3 +290,27 @@ for (const net of ["rail", "foreign"]) {
290
290
  )
291
291
  })
292
292
  }
293
+
294
+ for (const reversePath of [false, true]) {
295
+ test(`preserves a recovered branch endpoint when its rail moves (reversed: ${reversePath})`, () => {
296
+ const fixture = createFixture()
297
+ const supply = fixture.traces[1]!
298
+ supply.pins[0].y = 0.9
299
+ supply.tracePath[0]!.y = 0.9
300
+ supply.tracePath[1]!.y = 0.9
301
+ fixture.inputProblem.chips[3]!.center.y = 0.9
302
+ const recovered = fixture.traces[2]!
303
+ recovered.tracePath = recovered.tracePath.slice(0, 4)
304
+ if (reversePath) recovered.tracePath.reverse()
305
+ const result = alignSameNetJunctions(fixture)
306
+ const parallel = result.traces.find(
307
+ (trace) => trace.mspPairId === "parallel",
308
+ )!
309
+ const branch = result.traces.find((trace) => trace.mspPairId === "return")!
310
+ const junction = fixture.point(1.2, 0.9)
311
+ expect(tracePathContainsPoint(parallel.tracePath, junction)).toBe(true)
312
+ expect(tracePathContainsPoint(branch.tracePath, junction)).toBe(true)
313
+ expect(branch.tracePath).toContainEqual(fixture.point(2, -1))
314
+ expect(branch.tracePath).not.toContainEqual(fixture.point(1.2, 0.8))
315
+ })
316
+ }