@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
@@ -4,6 +4,7 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
4
4
  import type { InputProblem } from "lib/types/InputProblem"
5
5
  import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
6
6
  import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
7
+ import { isLabelRepresentedInline } from "./isLabelRepresentedInline"
7
8
 
8
9
  type StubDirection = "x+" | "x-" | "y+" | "y-"
9
10
 
@@ -126,13 +127,8 @@ export const alignPortOnlyInlineNetLabelStubs = ({
126
127
  }
127
128
 
128
129
  const alignedPlacements = [...placements]
129
- const supersededGlobalNetIds = new Set(
130
- placements.map((placement) => placement.globalConnNetId),
131
- )
132
130
  const retainedAnchoredLabelBounds = netLabelPlacements
133
- .filter(
134
- (placement) => !supersededGlobalNetIds.has(placement.globalConnNetId),
135
- )
131
+ .filter((placement) => !isLabelRepresentedInline(placement, placements))
136
132
  .map(getLabelBounds)
137
133
 
138
134
  for (const group of groups.values()) {
@@ -0,0 +1,64 @@
1
+ import type { InputProblem } from "lib/types/InputProblem"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+ import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
5
+
6
+ /** A terminal wire is separate from remote routes, even on the same net. */
7
+ export const getInlineTerminalBounds = (label: InlineNetLabelPlacement) => {
8
+ if (!label.stubTracePath) return null
9
+ // Give a line a nonzero box for the axis-aligned rectangle intersection test.
10
+ const epsilon = 1e-6
11
+ return {
12
+ minX: Math.min(...label.stubTracePath.map((p) => p.x)) - epsilon,
13
+ maxX: Math.max(...label.stubTracePath.map((p) => p.x)) + epsilon,
14
+ minY: Math.min(...label.stubTracePath.map((p) => p.y)) - epsilon,
15
+ maxY: Math.max(...label.stubTracePath.map((p) => p.y)) + epsilon,
16
+ }
17
+ }
18
+
19
+ /** Geometric obstacles for already placed inline text and its terminal wires. */
20
+ export const getInlineLabelObstacles = (
21
+ inputProblem: InputProblem,
22
+ placements: InlineNetLabelPlacement[],
23
+ ) => {
24
+ const fixedLabels: NetLabelPlacement[] = placements.map((label) => ({
25
+ globalConnNetId: label.globalConnNetId,
26
+ netId: label.netId,
27
+ pinIds: label.pinIds,
28
+ mspConnectionPairIds: [],
29
+ anchorPoint: label.anchorPoint,
30
+ orientation: "y+",
31
+ center: label.center,
32
+ width: label.axis === "x" ? label.width : label.height,
33
+ height: label.axis === "x" ? label.height : label.width,
34
+ }))
35
+ const terminalTraces: SolvedTracePath[] = placements.flatMap((label) => {
36
+ if (!label.stubTracePath) return []
37
+ const chip = inputProblem.chips.find((chip) =>
38
+ chip.pins.some((pin) => label.pinIds.includes(pin.pinId)),
39
+ )
40
+ const pin = chip?.pins.find((pin) => label.pinIds.includes(pin.pinId))
41
+ if (!chip || !pin) return []
42
+ const id = JSON.stringify([
43
+ "inline-terminal",
44
+ label.globalConnNetId,
45
+ [...label.pinIds].sort(),
46
+ ])
47
+ return [
48
+ {
49
+ mspPairId: id,
50
+ mspConnectionPairIds: [id],
51
+ globalConnNetId: label.globalConnNetId,
52
+ dcConnNetId: label.globalConnNetId,
53
+ pinIds: label.pinIds,
54
+ pins: [
55
+ { ...pin, chipId: chip.chipId },
56
+ { ...pin, ...label.stubTracePath[1], chipId: chip.chipId },
57
+ ],
58
+ tracePath: label.stubTracePath,
59
+ },
60
+ ]
61
+ })
62
+
63
+ return { fixedLabels, terminalTraces }
64
+ }
@@ -0,0 +1,407 @@
1
+ import type { Bounds, Point } from "@tscircuit/math-utils"
2
+ import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections"
3
+ import type { InputProblem } from "lib/types/InputProblem"
4
+ import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
5
+ import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
6
+ import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
7
+ import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
8
+ import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
9
+ import { getAnchoredNetLabelRenderedBounds } from "./getAnchoredNetLabelRenderedBounds"
10
+ import {
11
+ getInlineLabelObstacles,
12
+ getInlineTerminalBounds,
13
+ } from "./getInlineLabelObstacles"
14
+ import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver"
15
+ import type {
16
+ InlineNetLabelOutput,
17
+ InlineNetLabelPlacement,
18
+ } from "./InlineNetLabelSolver"
19
+
20
+ type Output = Omit<InlineNetLabelOutput, "inputProblem">
21
+ const EPS = 1e-6
22
+ const CLEARANCE = 0.05
23
+ const hits = (path: Point[], bounds: Bounds) =>
24
+ path.slice(1).some((p, i) => segmentIntersectsRect(path[i]!, p, bounds))
25
+ const same = (a: Point, b: Point) => Math.hypot(a.x - b.x, a.y - b.y) < EPS
26
+ const translate = (p: Point, axis: "x" | "y", distance: number) => ({
27
+ ...p,
28
+ [axis]: p[axis] + distance,
29
+ })
30
+ const intersections = (a: Point[], b: Point[]) =>
31
+ a.slice(1).flatMap((p, i) =>
32
+ b.slice(1).flatMap((q, j) => {
33
+ const point = getSegmentIntersection(a[i]!, p, b[j]!, q)
34
+ return point ? [point] : []
35
+ }),
36
+ )
37
+
38
+ /**
39
+ * Propose a sideways shift of an existing leg and its connected rail. Never add corners,
40
+ * route around a component, or search beyond the obstructing label column.
41
+ * A leaf power label may move sideways with its existing short connector.
42
+ * The caller must replan inline labels before committing a proposal.
43
+ */
44
+ export function* getLocalTraceLabelShifts(
45
+ inputProblem: InputProblem,
46
+ output: Output,
47
+ pendingInlinePlacements: InlineNetLabelPlacement[] = [],
48
+ ): Generator<Output> {
49
+ const placements = [
50
+ ...output.inlineNetLabelPlacements,
51
+ ...pendingInlinePlacements,
52
+ ]
53
+ const { fixedLabels, terminalTraces } = getInlineLabelObstacles(
54
+ inputProblem,
55
+ placements,
56
+ )
57
+ const terminalBounds = placements.flatMap((label) => {
58
+ const bounds = getInlineTerminalBounds(label)
59
+ return bounds ? [{ label, bounds }] : []
60
+ })
61
+ const obstacles = [
62
+ ...inputProblem.chips.map((chip) => ({
63
+ minX: chip.center.x - chip.width / 2,
64
+ maxX: chip.center.x + chip.width / 2,
65
+ minY: chip.center.y - chip.height / 2,
66
+ maxY: chip.center.y + chip.height / 2,
67
+ })),
68
+ ...(inputProblem.textBoxes ?? []).map((box) => getTextBoxBounds(box)),
69
+ ]
70
+ const inlineBounds = fixedLabels.map((label) => ({
71
+ minX: label.center.x - label.width / 2,
72
+ maxX: label.center.x + label.width / 2,
73
+ minY: label.center.y - label.height / 2,
74
+ maxY: label.center.y + label.height / 2,
75
+ }))
76
+ const pinPositions = [
77
+ ...inputProblem.chips.flatMap((chip) => chip.pins),
78
+ ...output.traces.flatMap((trace) => trace.pins),
79
+ ]
80
+ for (const trace of output.traces) {
81
+ const path = simplifyPath(trace.tracePath)
82
+ for (let i = 0; i < path.length - 1; i++) {
83
+ const a = path[i]!,
84
+ b = path[i + 1]!
85
+ const axis = Math.abs(a.x - b.x) < EPS ? "x" : "y"
86
+ const along = axis === "x" ? "y" : "x"
87
+ if (Math.abs(a[axis] - b[axis]) > EPS || same(a, b)) continue
88
+ const blocking = [
89
+ ...output.netLabelPlacements
90
+ .filter((label) => label.globalConnNetId !== trace.globalConnNetId)
91
+ .map(getAnchoredNetLabelRenderedBounds),
92
+ ...inlineBounds,
93
+ ...terminalBounds
94
+ .filter(
95
+ ({ label }) =>
96
+ !trace.pinIds.some((pinId) => label.pinIds.includes(pinId)),
97
+ )
98
+ .map(({ bounds }) => bounds),
99
+ ].filter((bounds) => segmentIntersectsRect(a, b, bounds))
100
+ if (!blocking.length) continue
101
+ // Cleanup can split one rail across several trace records, including a
102
+ // branch that starts at a junction instead of at its pin. Shift the
103
+ // connected collinear rail and its branch ends together.
104
+ let low = Math.min(a[along], b[along])
105
+ let high = Math.max(a[along], b[along])
106
+ let expanded = true
107
+ while (expanded) {
108
+ expanded = false
109
+ for (const other of output.traces) {
110
+ if (other.globalConnNetId !== trace.globalConnNetId) continue
111
+ for (let j = 1; j < other.tracePath.length; j++) {
112
+ const p = other.tracePath[j - 1]!,
113
+ q = other.tracePath[j]!
114
+ if (
115
+ Math.abs(p[axis] - a[axis]) > EPS ||
116
+ Math.abs(q[axis] - a[axis]) > EPS
117
+ )
118
+ continue
119
+ const min = Math.min(p[along], q[along]),
120
+ max = Math.max(p[along], q[along])
121
+ if (max < low - EPS || min > high + EPS) continue
122
+ if (min < low || max > high) expanded = true
123
+ low = Math.min(low, min)
124
+ high = Math.max(high, max)
125
+ }
126
+ }
127
+ }
128
+ const onRail = (p: Point) =>
129
+ Math.abs(p[axis] - a[axis]) < EPS &&
130
+ p[along] >= low - EPS &&
131
+ p[along] <= high + EPS
132
+ if (pinPositions.some(onRail)) continue
133
+ const minKey = axis === "x" ? "minX" : "minY"
134
+ const maxKey = axis === "x" ? "maxX" : "maxY"
135
+ const localWidth = Math.max(
136
+ ...blocking.map((bounds) => bounds[maxKey] - bounds[minKey]),
137
+ )
138
+ const railLabelBounds = output.netLabelPlacements
139
+ .filter(
140
+ (label) =>
141
+ label.globalConnNetId === trace.globalConnNetId &&
142
+ onRail(label.anchorPoint),
143
+ )
144
+ .map(getAnchoredNetLabelRenderedBounds)
145
+ const coordinates = [
146
+ ...new Set(
147
+ blocking.flatMap((bounds) => [
148
+ bounds[minKey] - CLEARANCE,
149
+ bounds[maxKey] + CLEARANCE,
150
+ ...railLabelBounds
151
+ .filter((own) => boundsOverlap(own, bounds))
152
+ .flatMap((own) => [
153
+ a[axis] + bounds[minKey] - CLEARANCE - own[maxKey],
154
+ a[axis] + bounds[maxKey] + CLEARANCE - own[minKey],
155
+ ]),
156
+ ]),
157
+ ),
158
+ ].sort((x, y) => Math.abs(x - a[axis]) - Math.abs(y - a[axis]))
159
+ for (const coordinate of coordinates) {
160
+ const distance = coordinate - a[axis]
161
+ if (Math.abs(distance) > localWidth + 2 * CLEARANCE) continue
162
+ const traces = output.traces.map((other) => {
163
+ if (
164
+ other.globalConnNetId !== trace.globalConnNetId ||
165
+ !other.tracePath.some(onRail)
166
+ )
167
+ return other
168
+ return {
169
+ ...other,
170
+ tracePath: other.tracePath.map((p) =>
171
+ onRail(p) ? translate(p, axis, distance) : p,
172
+ ),
173
+ }
174
+ })
175
+ const shiftedPaths = traces
176
+ .filter((other, index) => other !== output.traces[index])
177
+ .map((other) => other.tracePath)
178
+ let labels = output.netLabelPlacements.map((label) => {
179
+ if (
180
+ label.globalConnNetId !== trace.globalConnNetId ||
181
+ !onRail(label.anchorPoint)
182
+ )
183
+ return label
184
+ return {
185
+ ...label,
186
+ anchorPoint: translate(label.anchorPoint, axis, distance),
187
+ center: translate(label.center, axis, distance),
188
+ }
189
+ })
190
+ let blocked = false
191
+ const movedOwnLabelBounds = labels.flatMap((label, index) =>
192
+ label !== output.netLabelPlacements[index]
193
+ ? [getAnchoredNetLabelRenderedBounds(label)]
194
+ : [],
195
+ )
196
+ // A movable leaf label can make room for the shifted leg. Its last
197
+ // segment must already run parallel to that leg: extend the existing
198
+ // approach and translate the stem, retaining its orientation and bends.
199
+ for (const [labelIndex, label] of labels.entries()) {
200
+ if (label.globalConnNetId === trace.globalConnNetId) continue
201
+ const bounds = getAnchoredNetLabelRenderedBounds(label)
202
+ const clearanceEdges = movedOwnLabelBounds
203
+ .filter((own) => boundsOverlap(own, bounds))
204
+ .map((own) => own[distance > 0 ? maxKey : minKey])
205
+ if (
206
+ shiftedPaths.some((p) => hits(p, bounds)) &&
207
+ !output.traces.some(
208
+ (other) =>
209
+ other.globalConnNetId === trace.globalConnNetId &&
210
+ hits(other.tracePath, bounds),
211
+ )
212
+ )
213
+ clearanceEdges.push(coordinate)
214
+ if (!clearanceEdges.length) continue
215
+ const connectorIndex = traces.findIndex(
216
+ (candidate) =>
217
+ candidate.globalConnNetId === label.globalConnNetId &&
218
+ candidate.pinIds.length === 1 &&
219
+ candidate.tracePath.length === 3 &&
220
+ same(candidate.tracePath.at(-1)!, label.anchorPoint),
221
+ )
222
+ const connector = traces[connectorIndex]
223
+ if (!connector) {
224
+ const placementSolver = new NetLabelNetLabelCollisionSolver({
225
+ inputProblem,
226
+ traces: [...traces, ...terminalTraces],
227
+ netLabelPlacements: labels,
228
+ fixedNetLabelPlacements: fixedLabels,
229
+ useRenderedLabelBounds: true,
230
+ })
231
+ const nearby = placementSolver.getNearbyValidPlacements(
232
+ label,
233
+ localWidth + Math.max(label.width, label.height),
234
+ )[0]
235
+ if (!nearby) {
236
+ blocked = true
237
+ break
238
+ }
239
+ labels[labelIndex] = nearby
240
+ continue
241
+ }
242
+ const [start, bend, end] = connector.tracePath as [
243
+ Point,
244
+ Point,
245
+ Point,
246
+ ]
247
+ if (
248
+ Math.abs(bend[axis] - end[axis]) > EPS ||
249
+ Math.abs(start[along] - bend[along]) > EPS
250
+ ) {
251
+ blocked = true
252
+ break
253
+ }
254
+ const delta =
255
+ distance > 0
256
+ ? Math.max(...clearanceEdges) + CLEARANCE - bounds[minKey]
257
+ : Math.min(...clearanceEdges) - CLEARANCE - bounds[maxKey]
258
+ if (
259
+ Math.abs(delta) >
260
+ localWidth + bounds[maxKey] - bounds[minKey] + 2 * CLEARANCE
261
+ ) {
262
+ blocked = true
263
+ break
264
+ }
265
+ const moved = {
266
+ ...label,
267
+ anchorPoint: translate(label.anchorPoint, axis, delta),
268
+ center: translate(label.center, axis, delta),
269
+ }
270
+ labels[labelIndex] = moved
271
+ traces[connectorIndex] = {
272
+ ...connector,
273
+ tracePath: [start, translate(bend, axis, delta), moved.anchorPoint],
274
+ }
275
+ }
276
+ if (blocked) continue
277
+ for (const [index, proposed] of traces.entries()) {
278
+ const original = output.traces[index]!
279
+ if (proposed === original) continue
280
+ const candidate = proposed.tracePath
281
+ const previous = original.tracePath
282
+ if (
283
+ candidate
284
+ .slice(1)
285
+ .some(
286
+ (p, index) =>
287
+ Math.abs(p.x - candidate[index]!.x) > EPS &&
288
+ Math.abs(p.y - candidate[index]!.y) > EPS,
289
+ )
290
+ ) {
291
+ blocked = true
292
+ break
293
+ }
294
+ // Keep existing pin exits and all shared junctions. In particular,
295
+ // moving a rail must not disconnect a pin midway along it.
296
+ const dot = (a: Point, b: Point, c: Point, d: Point) =>
297
+ (b.x - a.x) * (d.x - c.x) + (b.y - a.y) * (d.y - c.y)
298
+ if (
299
+ dot(previous[0]!, previous[1]!, candidate[0]!, candidate[1]!) <=
300
+ EPS ||
301
+ dot(
302
+ previous.at(-1)!,
303
+ previous.at(-2)!,
304
+ candidate.at(-1)!,
305
+ candidate.at(-2)!,
306
+ ) <= EPS
307
+ ) {
308
+ blocked = true
309
+ break
310
+ }
311
+ const junctions = output.traces
312
+ .filter(
313
+ (other) =>
314
+ other !== original &&
315
+ other.globalConnNetId === original.globalConnNetId,
316
+ )
317
+ .flatMap((other) => [
318
+ ...other.tracePath,
319
+ ...intersections(previous, other.tracePath),
320
+ ])
321
+ .filter((p) => tracePathContainsPoint(previous, p))
322
+ .map((p) =>
323
+ original.globalConnNetId === trace.globalConnNetId && onRail(p)
324
+ ? translate(p, axis, distance)
325
+ : p,
326
+ )
327
+ const protectedPoints = [
328
+ ...pinPositions.filter((p) => tracePathContainsPoint(previous, p)),
329
+ ...junctions,
330
+ ]
331
+ if (
332
+ protectedPoints.some(
333
+ (p) => !tracePathContainsPoint(candidate, p),
334
+ ) ||
335
+ pinPositions.some((p) => {
336
+ const clearance = {
337
+ minX: p.x - CLEARANCE,
338
+ maxX: p.x + CLEARANCE,
339
+ minY: p.y - CLEARANCE,
340
+ maxY: p.y + CLEARANCE,
341
+ }
342
+ return hits(candidate, clearance) && !hits(previous, clearance)
343
+ }) ||
344
+ obstacles.some(
345
+ (bounds) => hits(candidate, bounds) && !hits(previous, bounds),
346
+ )
347
+ ) {
348
+ blocked = true
349
+ break
350
+ }
351
+ for (const other of [...traces, ...terminalTraces]) {
352
+ if (
353
+ other.mspPairId === proposed.mspPairId ||
354
+ other.globalConnNetId === proposed.globalConnNetId
355
+ )
356
+ continue
357
+ const originalOther =
358
+ output.traces.find((t) => t.mspPairId === other.mspPairId) ??
359
+ other
360
+ if (
361
+ doesPathCoincideWithTraces(candidate, [other]) ||
362
+ intersections(candidate, other.tracePath).length >
363
+ intersections(previous, originalOther.tracePath).length
364
+ ) {
365
+ blocked = true
366
+ break
367
+ }
368
+ }
369
+ if (blocked) break
370
+ }
371
+ if (blocked) continue
372
+ for (const [index, label] of labels.entries()) {
373
+ if (label === output.netLabelPlacements[index]) continue
374
+ const bounds = getAnchoredNetLabelRenderedBounds(label)
375
+ if (
376
+ obstacles.some((obstacle) => boundsOverlap(bounds, obstacle)) ||
377
+ inlineBounds.some((other) => boundsOverlap(bounds, other))
378
+ ) {
379
+ blocked = true
380
+ break
381
+ }
382
+ if (
383
+ labels.some(
384
+ (other, j) =>
385
+ index !== j &&
386
+ boundsOverlap(bounds, getAnchoredNetLabelRenderedBounds(other)),
387
+ )
388
+ ) {
389
+ blocked = true
390
+ break
391
+ }
392
+ if (
393
+ [...traces, ...terminalTraces].some(
394
+ (other) =>
395
+ other.globalConnNetId !== label.globalConnNetId &&
396
+ hits(other.tracePath, bounds),
397
+ )
398
+ ) {
399
+ blocked = true
400
+ break
401
+ }
402
+ }
403
+ if (!blocked) yield { ...output, traces, netLabelPlacements: labels }
404
+ }
405
+ }
406
+ }
407
+ }
@@ -0,0 +1,145 @@
1
+ import type { InlineNetLabelOutput } from "./InlineNetLabelSolver"
2
+ import { getAnchoredNetLabelRenderedBounds } from "./getAnchoredNetLabelRenderedBounds"
3
+ import { getInlineTerminalBounds } from "./getInlineLabelObstacles"
4
+ import { boundsOverlap } from "lib/utils/textBoxBounds"
5
+ import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
6
+
7
+ type LabelIdentity = { globalConnNetId: string; pinIds: string[] }
8
+ type TraceIdentity =
9
+ | { kind: "routed"; id: string }
10
+ | { kind: "terminal"; label: LabelIdentity }
11
+
12
+ export type OutputLabelCollision =
13
+ | { kind: "label-label"; labels: [LabelIdentity, LabelIdentity] }
14
+ | {
15
+ kind: "trace-label"
16
+ trace: TraceIdentity
17
+ label: LabelIdentity
18
+ target: "text" | "terminal"
19
+ }
20
+
21
+ const sameLabel = (a: LabelIdentity, b: LabelIdentity) => {
22
+ const first = [...a.pinIds].sort()
23
+ const second = [...b.pinIds].sort()
24
+ return (
25
+ a.globalConnNetId === b.globalConnNetId &&
26
+ first.length === second.length &&
27
+ first.every((pinId, index) => pinId === second[index])
28
+ )
29
+ }
30
+
31
+ export const sameOutputLabelCollision = (
32
+ a: OutputLabelCollision,
33
+ b: OutputLabelCollision,
34
+ ): boolean => {
35
+ if (a.kind === "label-label" && b.kind === "label-label")
36
+ return (
37
+ (sameLabel(a.labels[0], b.labels[0]) &&
38
+ sameLabel(a.labels[1], b.labels[1])) ||
39
+ (sameLabel(a.labels[0], b.labels[1]) &&
40
+ sameLabel(a.labels[1], b.labels[0]))
41
+ )
42
+ if (a.kind !== "trace-label" || b.kind !== "trace-label") return false
43
+ const sameTrace =
44
+ a.trace.kind === "routed" && b.trace.kind === "routed"
45
+ ? a.trace.id === b.trace.id
46
+ : a.trace.kind === "terminal" &&
47
+ b.trace.kind === "terminal" &&
48
+ sameLabel(a.trace.label, b.trace.label)
49
+ return sameTrace && a.target === b.target && sameLabel(a.label, b.label)
50
+ }
51
+
52
+ /** Identify final geometry conflicts without interpreting opaque identifiers. */
53
+ export const getOutputLabelCollisions = (
54
+ output: Omit<InlineNetLabelOutput, "inputProblem">,
55
+ ) => {
56
+ const labels = [
57
+ ...output.netLabelPlacements,
58
+ ...output.inlineNetLabelPlacements,
59
+ ].map((label) => ({
60
+ label,
61
+ bounds:
62
+ "axis" in label
63
+ ? {
64
+ minX:
65
+ label.center.x -
66
+ (label.axis === "x" ? label.width : label.height) / 2,
67
+ maxX:
68
+ label.center.x +
69
+ (label.axis === "x" ? label.width : label.height) / 2,
70
+ minY:
71
+ label.center.y -
72
+ (label.axis === "x" ? label.height : label.width) / 2,
73
+ maxY:
74
+ label.center.y +
75
+ (label.axis === "x" ? label.height : label.width) / 2,
76
+ }
77
+ : getAnchoredNetLabelRenderedBounds(label),
78
+ }))
79
+ const collisions: OutputLabelCollision[] = []
80
+ const add = (collision: OutputLabelCollision) => {
81
+ if (!collisions.some((other) => sameOutputLabelCollision(collision, other)))
82
+ collisions.push(collision)
83
+ }
84
+ for (let i = 0; i < labels.length; i++)
85
+ for (const other of labels.slice(i + 1)) {
86
+ if (boundsOverlap(labels[i]!.bounds, other.bounds))
87
+ add({ kind: "label-label", labels: [labels[i]!.label, other.label] })
88
+ }
89
+ const traces = [
90
+ ...output.traces.map((trace) => ({
91
+ ...trace,
92
+ identity: { kind: "routed", id: trace.mspPairId } satisfies TraceIdentity,
93
+ })),
94
+ ...output.inlineNetLabelPlacements.flatMap((label) =>
95
+ label.stubTracePath
96
+ ? [
97
+ {
98
+ identity: { kind: "terminal", label } satisfies TraceIdentity,
99
+ globalConnNetId: label.globalConnNetId,
100
+ tracePath: label.stubTracePath,
101
+ },
102
+ ]
103
+ : [],
104
+ ),
105
+ ]
106
+ for (const trace of traces)
107
+ for (const { label, bounds } of labels) {
108
+ if (
109
+ ("axis" in label || label.globalConnNetId !== trace.globalConnNetId) &&
110
+ trace.tracePath
111
+ .slice(1)
112
+ .some((end, index) =>
113
+ segmentIntersectsRect(trace.tracePath[index]!, end, bounds),
114
+ )
115
+ )
116
+ add({
117
+ kind: "trace-label",
118
+ trace: trace.identity,
119
+ label,
120
+ target: "text",
121
+ })
122
+ }
123
+ for (const label of output.inlineNetLabelPlacements) {
124
+ const bounds = getInlineTerminalBounds(label)
125
+ if (!bounds) continue
126
+ for (const trace of output.traces) {
127
+ // A route attached to the terminal's own pin is an intentional connection.
128
+ if (trace.pinIds.some((pinId) => label.pinIds.includes(pinId))) continue
129
+ if (
130
+ trace.tracePath
131
+ .slice(1)
132
+ .some((end, index) =>
133
+ segmentIntersectsRect(trace.tracePath[index]!, end, bounds),
134
+ )
135
+ )
136
+ add({
137
+ kind: "trace-label",
138
+ trace: { kind: "routed", id: trace.mspPairId },
139
+ label,
140
+ target: "terminal",
141
+ })
142
+ }
143
+ }
144
+ return collisions
145
+ }
@@ -0,0 +1,15 @@
1
+ import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
2
+
3
+ /** Every pin represented by the old label must still have the same net name. */
4
+ export const isLabelRepresentedInline = (
5
+ label: { globalConnNetId: string; pinIds: string[] },
6
+ inlinePlacements: InlineNetLabelPlacement[],
7
+ ) =>
8
+ label.pinIds.length > 0 &&
9
+ label.pinIds.every((pinId) =>
10
+ inlinePlacements.some(
11
+ (inline) =>
12
+ inline.globalConnNetId === label.globalConnNetId &&
13
+ inline.pinIds.includes(pinId),
14
+ ),
15
+ )