@tscircuit/schematic-trace-solver 0.0.187 → 0.0.189
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +42 -23
- package/dist/index.js +1053 -316
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +612 -230
- package/lib/solvers/InlineNetLabelSolver/alignPortOnlyInlineNetLabelStubs.ts +2 -6
- package/lib/solvers/InlineNetLabelSolver/getAnchoredNetLabelRenderedBounds.ts +13 -0
- package/lib/solvers/InlineNetLabelSolver/getInlineLabelObstacles.ts +64 -0
- package/lib/solvers/InlineNetLabelSolver/getLocalTraceLabelShifts.ts +407 -0
- package/lib/solvers/InlineNetLabelSolver/getOutputLabelCollisions.ts +145 -0
- package/lib/solvers/InlineNetLabelSolver/isLabelRepresentedInline.ts +15 -0
- package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +61 -31
- package/lib/solvers/NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver.ts +94 -16
- package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +4 -3
- package/lib/solvers/SameNetJunctionAlignmentSolver/alignSameNetJunctions.ts +7 -7
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +3 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +5 -1
- package/lib/types/InputProblem.ts +2 -2
- package/package.json +1 -1
- package/site/SchematicTracePipelineSolver/repro-robot-controller-imu-tof.page.tsx +7 -0
- package/site/bug-reports/bug-report-20260907T110144Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260907T145640Z.page.tsx +4 -0
- package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +18 -18
- package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260819T091818Z/bug-report-20260819T091818Z.test.ts +21 -0
- package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +5 -38
- package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +3 -9
- package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +18 -30
- package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.test.ts +33 -0
- package/tests/bug-reports/bug-report-20260907T110144Z/__snapshots__/bug-report-20260907T110144Z.snap.svg +282 -0
- package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.json +1073 -0
- package/tests/bug-reports/bug-report-20260907T110144Z/bug-report-20260907T110144Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260907T145640Z/__snapshots__/bug-report-20260907T145640Z.snap.svg +1107 -0
- package/tests/bug-reports/bug-report-20260907T145640Z/bug-report-20260907T145640Z.json +3563 -0
- package/tests/bug-reports/bug-report-20260907T145640Z/bug-report-20260907T145640Z.test.ts +117 -0
- package/tests/repros/__snapshots__/repro-battery-management-bq24073.snap.svg +4 -4
- package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +20 -20
- package/tests/repros/__snapshots__/repro-esp12f-section.snap.svg +37 -55
- package/tests/repros/__snapshots__/repro-hub-usb-sheet.snap.svg +44 -68
- package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +3 -9
- package/tests/repros/__snapshots__/repro-robot-controller-imu-tof.snap.svg +199 -0
- package/tests/repros/__snapshots__/repro-robot-controller-imu-tof.source.svg +33 -0
- package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +50 -74
- package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +2 -2
- package/tests/repros/__snapshots__/rp2040-robot-controller-disconnected-gnd.snap.svg +103 -0
- package/tests/repros/assets/repro-robot-controller-imu-tof.input.json +540 -0
- package/tests/repros/assets/rp2040-robot-controller.input.json +349 -0
- package/tests/repros/repro-battery-management-bq24073.test.ts +13 -1
- package/tests/repros/repro-hub-usb-sheet.test.ts +25 -0
- package/tests/repros/repro-robot-controller-imu-tof.test.ts +83 -0
- package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +59 -13
- package/tests/repros/rp2040-robot-controller-disconnected-gnd.test.ts +59 -0
- package/tests/solvers/InlineNetLabelSolver/get-anchored-net-label-rendered-bounds.test.ts +24 -0
- package/tests/solvers/InlineNetLabelSolver/local-trace-label-shifts.test.ts +296 -0
- package/tests/solvers/InlineNetLabelSolver/multi-pin-connected-components.test.ts +7 -4
- package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +2 -1
- package/tests/solvers/InlineNetLabelSolver/shared-direct-and-named-connection.test.ts +62 -0
- package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +9 -3
- package/tests/solvers/TraceCleanupSolver/perpendicular-detours-preserve-endpoints.test.ts +60 -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()) {
|
|
@@ -19,6 +19,19 @@ export const getAnchoredNetLabelRenderedBounds = (
|
|
|
19
19
|
placement: NetLabelPlacement,
|
|
20
20
|
): Bounds => {
|
|
21
21
|
if (placement.orientation !== "x-" && placement.orientation !== "x+") {
|
|
22
|
+
// Named vertical tags render their long text extent along y. Width-only
|
|
23
|
+
// placeholders retain the supplied envelope instead.
|
|
24
|
+
if (placement.netLabelText && placement.width > placement.height) {
|
|
25
|
+
const width = placement.height
|
|
26
|
+
const height = placement.width
|
|
27
|
+
const center = getCenterFromAnchor(
|
|
28
|
+
placement.anchorPoint,
|
|
29
|
+
placement.orientation,
|
|
30
|
+
width,
|
|
31
|
+
height,
|
|
32
|
+
)
|
|
33
|
+
return getRectBounds(center, width, height)
|
|
34
|
+
}
|
|
22
35
|
return getRectBounds(placement.center, placement.width, placement.height)
|
|
23
36
|
}
|
|
24
37
|
|
|
@@ -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
|
+
)
|