@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.
Files changed (28) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +195 -74
  3. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +16 -1
  4. package/lib/solvers/InlineNetLabelSolver/restoreReroutesAroundSupersededLabels.ts +174 -0
  5. package/lib/solvers/NetLabelToTraceSolver/NetLabelToTraceSolver.ts +2 -30
  6. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +9 -0
  7. package/lib/utils/doesPathCoincideWithTraces.ts +17 -4
  8. package/lib/utils/pathIntersectsRenderedLabel.ts +33 -0
  9. package/package.json +1 -1
  10. package/site/bug-reports/bug-report-20260901T055358Z.page.tsx +4 -0
  11. package/site/bug-reports/bug-report-20260901T064117Z.page.tsx +4 -0
  12. package/site/bug-reports/bug-report-20260901T134241Z.page.tsx +4 -0
  13. package/tests/bug-reports/bug-report-20260901T055358Z/__snapshots__/bug-report-20260901T055358Z.snap.svg +676 -0
  14. package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.json +2467 -0
  15. package/tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.test.ts +59 -0
  16. package/tests/bug-reports/bug-report-20260901T064117Z/__snapshots__/bug-report-20260901T064117Z.snap.svg +134 -0
  17. package/tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.json +215 -0
  18. package/tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.test.ts +12 -0
  19. package/tests/bug-reports/bug-report-20260901T134241Z/__snapshots__/bug-report-20260901T134241Z.snap.svg +115 -0
  20. package/tests/bug-reports/bug-report-20260901T134241Z/bug-report-20260901T134241Z.json +359 -0
  21. package/tests/bug-reports/bug-report-20260901T134241Z/bug-report-20260901T134241Z.test.ts +12 -0
  22. package/tests/repros/__snapshots__/repro-trellis-core-gnd-net-label-overlap.snap.svg +134 -0
  23. package/tests/repros/__snapshots__/repro-wireless-mouse-charger-section.snap.svg +164 -0
  24. package/tests/repros/assets/repro-trellis-core-gnd-net-label-overlap.input.json +178 -0
  25. package/tests/repros/assets/repro-wireless-mouse-charger-section.input.json +591 -0
  26. package/tests/repros/repro-trellis-core-gnd-net-label-overlap.test.ts +73 -0
  27. package/tests/repros/repro-wireless-mouse-charger-section.test.ts +16 -0
  28. 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 trace of traces) {
43
- for (let j = 0; j < trace.tracePath.length - 1; j++) {
44
- const traceSegStart = trace.tracePath[j]!
45
- const traceSegEnd = trace.tracePath[j + 1]!
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
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.174",
8
+ "version": "0.0.175",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260901T055358Z/bug-report-20260901T055358Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260901T064117Z/bug-report-20260901T064117Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260901T134241Z/bug-report-20260901T134241Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />