@tscircuit/schematic-trace-solver 0.0.174 → 0.0.175
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 +2 -0
- package/dist/index.js +195 -74
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +16 -1
- package/lib/solvers/InlineNetLabelSolver/restoreReroutesAroundSupersededLabels.ts +174 -0
- package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +2 -30
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +9 -0
- package/lib/utils/doesPathCoincideWithTraces.ts +17 -4
- package/lib/utils/pathIntersectsRenderedLabel.ts +33 -0
- package/package.json +1 -1
- package/site/bug-reports/bug-report-20260901T055358Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260901T064117Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260901T134241Z.page.tsx +4 -0
- package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +676 -0
- package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.json +2467 -0
- package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.test.ts +59 -0
- package/tests/bug-reports/bug-report-20260901T064117Z/__snapshots__/bug-report-20260901T064117Z.snap.svg +134 -0
- package/tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.json +215 -0
- package/tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260901T134241Z/__snapshots__/bug-report-20260901T134241Z.snap.svg +115 -0
- package/tests/bug-reports/bug-report-20260901T134241Z/bug-report-20260901T134241Z.json +359 -0
- package/tests/bug-reports/bug-report-20260901T134241Z/bug-report-20260901T134241Z.test.ts +12 -0
- package/tests/repros/__snapshots__/repro-trellis-core-gnd-net-label-overlap.snap.svg +134 -0
- package/tests/repros/__snapshots__/repro-wireless-mouse-charger-section.snap.svg +164 -0
- package/tests/repros/assets/repro-trellis-core-gnd-net-label-overlap.input.json +178 -0
- package/tests/repros/assets/repro-wireless-mouse-charger-section.input.json +591 -0
- package/tests/repros/repro-trellis-core-gnd-net-label-overlap.test.ts +73 -0
- package/tests/repros/repro-wireless-mouse-charger-section.test.ts +16 -0
- package/tests/solvers/InlineNetLabelSolver/restore-reroutes-around-superseded-labels.test.ts +186 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections"
|
|
3
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
4
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
5
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
6
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
7
|
+
import { preservesLabelAnchors } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors"
|
|
8
|
+
import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
|
|
9
|
+
import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
|
|
10
|
+
import type { CompletedTraceReroute } from "lib/solvers/TraceElbowTransitionSimplificationSolver/types"
|
|
11
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
12
|
+
import {
|
|
13
|
+
doesPathCoincideWithPaths,
|
|
14
|
+
doesPathCoincideWithTraces,
|
|
15
|
+
} from "lib/utils/doesPathCoincideWithTraces"
|
|
16
|
+
import { pathIntersectsRenderedLabel } from "lib/utils/pathIntersectsRenderedLabel"
|
|
17
|
+
import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
|
|
18
|
+
|
|
19
|
+
const EPS = 1e-6
|
|
20
|
+
|
|
21
|
+
const getPathLength = (path: Point[]) =>
|
|
22
|
+
path.slice(1).reduce((length, point, pointIndex) => {
|
|
23
|
+
const previousPoint = path[pointIndex]!
|
|
24
|
+
return (
|
|
25
|
+
length +
|
|
26
|
+
Math.abs(point.x - previousPoint.x) +
|
|
27
|
+
Math.abs(point.y - previousPoint.y)
|
|
28
|
+
)
|
|
29
|
+
}, 0)
|
|
30
|
+
|
|
31
|
+
const pathsEqual = (first: Point[], second: Point[]) =>
|
|
32
|
+
first.length === second.length &&
|
|
33
|
+
first.every(
|
|
34
|
+
(point, index) =>
|
|
35
|
+
Math.abs(point.x - second[index]!.x) <= EPS &&
|
|
36
|
+
Math.abs(point.y - second[index]!.y) <= EPS,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const isStrictlySimpler = (candidate: Point[], current: Point[]) =>
|
|
40
|
+
candidate.length < current.length &&
|
|
41
|
+
getPathLength(candidate) <= getPathLength(current) + EPS
|
|
42
|
+
|
|
43
|
+
const getIntersectionKeys = (path: Point[], otherPath: Point[]) => {
|
|
44
|
+
const intersections = new Set<string>()
|
|
45
|
+
for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) {
|
|
46
|
+
for (
|
|
47
|
+
let otherPathIndex = 0;
|
|
48
|
+
otherPathIndex < otherPath.length - 1;
|
|
49
|
+
otherPathIndex++
|
|
50
|
+
) {
|
|
51
|
+
const point = getSegmentIntersection(
|
|
52
|
+
path[pathIndex]!,
|
|
53
|
+
path[pathIndex + 1]!,
|
|
54
|
+
otherPath[otherPathIndex]!,
|
|
55
|
+
otherPath[otherPathIndex + 1]!,
|
|
56
|
+
)
|
|
57
|
+
if (point) {
|
|
58
|
+
intersections.add(`${point.x.toFixed(6)},${point.y.toFixed(6)}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return intersections
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const introducesNewCrossings = (
|
|
66
|
+
candidatePath: Point[],
|
|
67
|
+
currentPath: Point[],
|
|
68
|
+
otherPaths: Point[][],
|
|
69
|
+
) =>
|
|
70
|
+
otherPaths.some((otherPath) => {
|
|
71
|
+
const currentIntersections = getIntersectionKeys(currentPath, otherPath)
|
|
72
|
+
const candidateIntersections = getIntersectionKeys(candidatePath, otherPath)
|
|
73
|
+
return [...candidateIntersections].some(
|
|
74
|
+
(intersection) => !currentIntersections.has(intersection),
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Restores a trace's recorded pre-collision path when the anchored net label
|
|
80
|
+
* that caused its reroute was later replaced by an inline label.
|
|
81
|
+
*
|
|
82
|
+
* A reroute is unwound only when its current geometry still matches the
|
|
83
|
+
* recorded result and the original path is strictly simpler without creating
|
|
84
|
+
* collisions, crossings, overlaps, or detached label anchors.
|
|
85
|
+
*/
|
|
86
|
+
export const restoreReroutesAroundSupersededLabels = ({
|
|
87
|
+
inputProblem,
|
|
88
|
+
traces,
|
|
89
|
+
netLabelPlacements,
|
|
90
|
+
inlineNetLabelPlacements,
|
|
91
|
+
completedReroutes,
|
|
92
|
+
}: {
|
|
93
|
+
inputProblem: InputProblem
|
|
94
|
+
traces: SolvedTracePath[]
|
|
95
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
96
|
+
inlineNetLabelPlacements: InlineNetLabelPlacement[]
|
|
97
|
+
completedReroutes: CompletedTraceReroute[]
|
|
98
|
+
}) => {
|
|
99
|
+
const outputTraces = [...traces]
|
|
100
|
+
const supersededLabelGlobalConnNetIds = new Set(
|
|
101
|
+
inlineNetLabelPlacements.map((placement) => placement.globalConnNetId),
|
|
102
|
+
)
|
|
103
|
+
const obstacles = getObstacleRects(inputProblem)
|
|
104
|
+
let restoredTraceCount = 0
|
|
105
|
+
|
|
106
|
+
// A trace can be rerouted more than once. Unwind only unchanged reroutes,
|
|
107
|
+
// newest first, so every restoration has exact provenance.
|
|
108
|
+
for (const reroute of [...completedReroutes].reverse()) {
|
|
109
|
+
if (!supersededLabelGlobalConnNetIds.has(reroute.label.globalConnNetId)) {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const traceIndex = outputTraces.findIndex(
|
|
114
|
+
(trace) => trace.mspPairId === reroute.initialTrace.mspPairId,
|
|
115
|
+
)
|
|
116
|
+
if (traceIndex < 0) continue
|
|
117
|
+
|
|
118
|
+
const currentTrace = outputTraces[traceIndex]!
|
|
119
|
+
const currentPath = simplifyPath(currentTrace.tracePath)
|
|
120
|
+
const recordedReroutePath = simplifyPath(reroute.reroutedTracePath)
|
|
121
|
+
if (!pathsEqual(currentPath, recordedReroutePath)) continue
|
|
122
|
+
|
|
123
|
+
const candidatePath = simplifyPath(reroute.initialTrace.tracePath)
|
|
124
|
+
if (!isStrictlySimpler(candidatePath, currentPath)) continue
|
|
125
|
+
|
|
126
|
+
const candidateTrace = { ...currentTrace, tracePath: candidatePath }
|
|
127
|
+
const otherNetTraces = outputTraces.filter(
|
|
128
|
+
(trace) =>
|
|
129
|
+
trace.mspPairId !== currentTrace.mspPairId &&
|
|
130
|
+
trace.globalConnNetId !== currentTrace.globalConnNetId,
|
|
131
|
+
)
|
|
132
|
+
const otherNetInlineLabels = inlineNetLabelPlacements.filter(
|
|
133
|
+
(label) => label.globalConnNetId !== currentTrace.globalConnNetId,
|
|
134
|
+
)
|
|
135
|
+
const otherNetInlineStubPaths = otherNetInlineLabels.flatMap((label) =>
|
|
136
|
+
label.stubTracePath ? [[...label.stubTracePath]] : [],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
isPathCollidingWithObstacles(candidatePath, obstacles) ||
|
|
141
|
+
detectTraceLabelOverlap({
|
|
142
|
+
traces: [candidateTrace],
|
|
143
|
+
netLabels: netLabelPlacements,
|
|
144
|
+
}).length > 0 ||
|
|
145
|
+
otherNetInlineLabels.some((label) =>
|
|
146
|
+
pathIntersectsRenderedLabel(candidatePath, label),
|
|
147
|
+
) ||
|
|
148
|
+
doesPathCoincideWithTraces(candidatePath, otherNetTraces) ||
|
|
149
|
+
doesPathCoincideWithPaths(candidatePath, otherNetInlineStubPaths) ||
|
|
150
|
+
introducesNewCrossings(
|
|
151
|
+
candidatePath,
|
|
152
|
+
currentPath,
|
|
153
|
+
otherNetTraces.map((trace) => trace.tracePath),
|
|
154
|
+
) ||
|
|
155
|
+
introducesNewCrossings(
|
|
156
|
+
candidatePath,
|
|
157
|
+
currentPath,
|
|
158
|
+
otherNetInlineStubPaths,
|
|
159
|
+
) ||
|
|
160
|
+
!preservesLabelAnchors(
|
|
161
|
+
netLabelPlacements,
|
|
162
|
+
[currentTrace],
|
|
163
|
+
[candidateTrace],
|
|
164
|
+
)
|
|
165
|
+
) {
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
outputTraces[traceIndex] = candidateTrace
|
|
170
|
+
restoredTraceCount++
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { traces: outputTraces, restoredTraceCount }
|
|
174
|
+
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
doesSegmentIntersectRect,
|
|
3
|
-
getBoundFromCenteredRect,
|
|
4
|
-
type Point,
|
|
5
|
-
} from "@tscircuit/math-utils"
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
6
2
|
import type { GraphicsObject } from "graphics-debug"
|
|
7
3
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
8
4
|
import { doesPairCrossRestrictedCenterLines } from "lib/solvers/MspConnectionPairSolver/doesPairCrossRestrictedCenterLines"
|
|
@@ -24,6 +20,7 @@ import type {
|
|
|
24
20
|
PinId,
|
|
25
21
|
} from "lib/types/InputProblem"
|
|
26
22
|
import { arePinsInDifferentSchematicSections } from "lib/utils/arePinsInDifferentSchematicSections"
|
|
23
|
+
import { pathIntersectsRenderedLabel } from "lib/utils/pathIntersectsRenderedLabel"
|
|
27
24
|
import {
|
|
28
25
|
type InlineNetLabelOutput,
|
|
29
26
|
type InlineNetLabelPlacement,
|
|
@@ -55,31 +52,6 @@ const MAX_ROUTED_COMPONENT_RECOVERY_PERPENDICULAR_OFFSET = 0.25
|
|
|
55
52
|
const getCanonicalPairKey = (firstPinId: PinId, secondPinId: PinId) =>
|
|
56
53
|
[firstPinId, secondPinId].sort().join("--")
|
|
57
54
|
|
|
58
|
-
export const pathIntersectsRenderedLabel = (
|
|
59
|
-
path: Point[],
|
|
60
|
-
label: NetLabelPlacement | InlineNetLabelPlacement,
|
|
61
|
-
) => {
|
|
62
|
-
let width = label.width
|
|
63
|
-
let height = label.height
|
|
64
|
-
if ("axis" in label && label.axis === "y") {
|
|
65
|
-
width = label.height
|
|
66
|
-
height = label.width
|
|
67
|
-
}
|
|
68
|
-
const bounds = getBoundFromCenteredRect({
|
|
69
|
-
center: label.center,
|
|
70
|
-
width,
|
|
71
|
-
height,
|
|
72
|
-
})
|
|
73
|
-
for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) {
|
|
74
|
-
if (
|
|
75
|
-
doesSegmentIntersectRect(path[pathIndex]!, path[pathIndex + 1]!, bounds)
|
|
76
|
-
) {
|
|
77
|
-
return true
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return false
|
|
81
|
-
}
|
|
82
|
-
|
|
83
55
|
const getPerpendicularOffset = (firstPoint: Point, secondPoint: Point) => {
|
|
84
56
|
const xDistance = Math.abs(firstPoint.x - secondPoint.x)
|
|
85
57
|
const yDistance = Math.abs(firstPoint.y - secondPoint.y)
|
|
@@ -581,11 +581,20 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
581
581
|
(instance) => {
|
|
582
582
|
const junctionOutput =
|
|
583
583
|
instance.sameNetJunctionAlignmentSolver!.getOutput()
|
|
584
|
+
const completedReroutes = [
|
|
585
|
+
...instance.traceLabelOverlapAvoidanceSolver!.getOutput()
|
|
586
|
+
.completedReroutes,
|
|
587
|
+
...instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
|
|
588
|
+
.completedReroutes,
|
|
589
|
+
...instance.netLabelTraceCollisionSolver!.getOutput()
|
|
590
|
+
.completedReroutes,
|
|
591
|
+
]
|
|
584
592
|
return [
|
|
585
593
|
{
|
|
586
594
|
inputProblem: instance.inputProblem,
|
|
587
595
|
traces: junctionOutput.traces,
|
|
588
596
|
netLabelPlacements: junctionOutput.netLabelPlacements,
|
|
597
|
+
completedReroutes,
|
|
589
598
|
},
|
|
590
599
|
]
|
|
591
600
|
},
|
|
@@ -22,6 +22,19 @@ export const SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE =
|
|
|
22
22
|
export const doesPathCoincideWithTraces = (
|
|
23
23
|
path: Point[],
|
|
24
24
|
traces: SolvedTracePath[],
|
|
25
|
+
): boolean =>
|
|
26
|
+
doesPathCoincideWithPaths(
|
|
27
|
+
path,
|
|
28
|
+
traces.map((trace) => trace.tracePath),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Path-only form for rendered wire geometry that is not represented by a
|
|
33
|
+
* SolvedTracePath, such as inline-label terminal stubs.
|
|
34
|
+
*/
|
|
35
|
+
export const doesPathCoincideWithPaths = (
|
|
36
|
+
path: Point[],
|
|
37
|
+
otherPaths: Point[][],
|
|
25
38
|
): boolean => {
|
|
26
39
|
const rangesOverlap1D = (a1: number, a2: number, b1: number, b2: number) =>
|
|
27
40
|
Math.min(Math.max(a1, a2), Math.max(b1, b2)) -
|
|
@@ -39,10 +52,10 @@ export const doesPathCoincideWithTraces = (
|
|
|
39
52
|
const crossAxis = isVertical ? "x" : "y"
|
|
40
53
|
const alongAxis = isVertical ? "y" : "x"
|
|
41
54
|
|
|
42
|
-
for (const
|
|
43
|
-
for (let j = 0; j <
|
|
44
|
-
const traceSegStart =
|
|
45
|
-
const traceSegEnd =
|
|
55
|
+
for (const otherPath of otherPaths) {
|
|
56
|
+
for (let j = 0; j < otherPath.length - 1; j++) {
|
|
57
|
+
const traceSegStart = otherPath[j]!
|
|
58
|
+
const traceSegEnd = otherPath[j + 1]!
|
|
46
59
|
|
|
47
60
|
const isParallel =
|
|
48
61
|
Math.abs(traceSegStart[crossAxis] - traceSegEnd[crossAxis]) <
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
doesSegmentIntersectRect,
|
|
3
|
+
getBoundFromCenteredRect,
|
|
4
|
+
type Point,
|
|
5
|
+
} from "@tscircuit/math-utils"
|
|
6
|
+
import type { InlineNetLabelPlacement } from "lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver"
|
|
7
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
8
|
+
|
|
9
|
+
/** Returns whether a path crosses the rendered text bounds of a net label. */
|
|
10
|
+
export const pathIntersectsRenderedLabel = (
|
|
11
|
+
path: Point[],
|
|
12
|
+
label: NetLabelPlacement | InlineNetLabelPlacement,
|
|
13
|
+
) => {
|
|
14
|
+
let width = label.width
|
|
15
|
+
let height = label.height
|
|
16
|
+
if ("axis" in label && label.axis === "y") {
|
|
17
|
+
width = label.height
|
|
18
|
+
height = label.width
|
|
19
|
+
}
|
|
20
|
+
const bounds = getBoundFromCenteredRect({
|
|
21
|
+
center: label.center,
|
|
22
|
+
width,
|
|
23
|
+
height,
|
|
24
|
+
})
|
|
25
|
+
for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) {
|
|
26
|
+
if (
|
|
27
|
+
doesSegmentIntersectRect(path[pathIndex]!, path[pathIndex + 1]!, bounds)
|
|
28
|
+
) {
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false
|
|
33
|
+
}
|
package/package.json
CHANGED