@tscircuit/schematic-trace-solver 0.0.37 → 0.0.39

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 (35) hide show
  1. package/dist/index.d.ts +25 -1
  2. package/dist/index.js +917 -10
  3. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +17 -0
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +31 -12
  5. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +40 -0
  6. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +240 -0
  7. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/balanceLShapes.ts +207 -0
  8. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/countTurns.ts +18 -0
  9. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +38 -0
  10. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/hasCollisions.ts +22 -0
  11. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/hasCollisionsWithLabels.ts +19 -0
  12. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/index.ts +1 -0
  13. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/minimizeTurnsWithFilteredLabels.ts +66 -0
  14. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +73 -0
  15. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/simplifyPath.ts +41 -0
  16. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryConnectPoints.ts +14 -0
  17. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts +229 -0
  18. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/turnMinimization.ts +211 -0
  19. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/violation.ts +25 -0
  20. package/package.json +1 -1
  21. package/site/examples/example20.page.tsx +103 -0
  22. package/site/examples/example22.page.tsx +108 -0
  23. package/site/examples/example23.page.tsx +137 -0
  24. package/site/examples/example24.page.tsx +128 -0
  25. package/tests/examples/__snapshots__/example16.snap.svg +27 -27
  26. package/tests/examples/__snapshots__/example22.snap.svg +137 -0
  27. package/tests/examples/__snapshots__/example23.snap.svg +137 -0
  28. package/tests/examples/__snapshots__/example24.snap.svg +143 -0
  29. package/tests/examples/__snapshots__/example25.snap.svg +165 -0
  30. package/tests/examples/__snapshots__/example26.snap.svg +157 -0
  31. package/tests/examples/example22.test.tsx +109 -0
  32. package/tests/examples/example23.test.tsx +109 -0
  33. package/tests/examples/example24.test.tsx +114 -0
  34. package/tests/examples/example25.test.tsx +143 -0
  35. package/tests/examples/example26.test.tsx +134 -0
@@ -0,0 +1,19 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { segmentIntersectsRect } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
3
+
4
+ export const hasCollisionsWithLabels = (
5
+ pathSegments: Point[],
6
+ labels: any[],
7
+ ): boolean => {
8
+ for (let i = 0; i < pathSegments.length - 1; i++) {
9
+ const p1 = pathSegments[i]
10
+ const p2 = pathSegments[i + 1]
11
+
12
+ for (const label of labels) {
13
+ if (segmentIntersectsRect(p1, p2, label)) {
14
+ return true
15
+ }
16
+ }
17
+ }
18
+ return false
19
+ }
@@ -0,0 +1 @@
1
+ export * from "./TraceLabelOverlapAvoidanceSolver"
@@ -0,0 +1,66 @@
1
+ import type { InputProblem } from "lib/types/InputProblem"
2
+ import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+ import { getObstacleRects } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
+ import { minimizeTurns } from "./turnMinimization"
6
+
7
+ export const minimizeTurnsWithFilteredLabels = ({
8
+ traces,
9
+ problem,
10
+ allLabelPlacements,
11
+ mergedLabelNetIdMap,
12
+ paddingBuffer,
13
+ }: {
14
+ traces: SolvedTracePath[]
15
+ problem: InputProblem
16
+ allLabelPlacements: NetLabelPlacement[]
17
+ mergedLabelNetIdMap: Record<string, Set<string>>
18
+ paddingBuffer: number
19
+ }): SolvedTracePath[] | null => {
20
+ let changesMade = false
21
+ const obstacles = getObstacleRects(problem)
22
+
23
+ const newTraces = traces.map((trace) => {
24
+ const originalPath = trace.tracePath
25
+ const filteredLabels = allLabelPlacements.filter((label) => {
26
+ const originalNetIds = mergedLabelNetIdMap[label.globalConnNetId]
27
+ if (originalNetIds) {
28
+ return !originalNetIds.has(trace.globalConnNetId)
29
+ }
30
+ return label.globalConnNetId !== trace.globalConnNetId
31
+ })
32
+
33
+ const labelBounds = filteredLabels.map((nl) => ({
34
+ minX: nl.center.x - nl.width / 2 - paddingBuffer,
35
+ maxX: nl.center.x + nl.width / 2 + paddingBuffer,
36
+ minY: nl.center.y - nl.height / 2 - paddingBuffer,
37
+ maxY: nl.center.y + nl.height / 2 + paddingBuffer,
38
+ }))
39
+
40
+ const newPath = minimizeTurns({
41
+ path: originalPath,
42
+ obstacles,
43
+ labelBounds,
44
+ })
45
+
46
+ if (
47
+ newPath.length !== originalPath.length ||
48
+ newPath.some(
49
+ (p, i) => p.x !== originalPath[i].x || p.y !== originalPath[i].y,
50
+ )
51
+ ) {
52
+ changesMade = true
53
+ }
54
+
55
+ return {
56
+ ...trace,
57
+ tracePath: newPath,
58
+ }
59
+ })
60
+
61
+ if (changesMade) {
62
+ return newTraces
63
+ } else {
64
+ return null
65
+ }
66
+ }
@@ -0,0 +1,73 @@
1
+ import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
2
+ import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import { getRectBounds } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
4
+ import { getObstacleRects } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
5
+ import type { InputProblem } from "lib/types/InputProblem"
6
+ import { findTraceViolationZone } from "./violation"
7
+ import { tryFourPointDetour, trySnipAndReconnect } from "./trySnipAndReconnect"
8
+ import { simplifyPath } from "./simplifyPath"
9
+
10
+ export const rerouteCollidingTrace = ({
11
+ trace,
12
+ label,
13
+ problem,
14
+ paddingBuffer,
15
+ detourCount,
16
+ }: {
17
+ trace: SolvedTracePath
18
+ label: NetLabelPlacement
19
+ problem: InputProblem
20
+ paddingBuffer: number
21
+ detourCount: number
22
+ }): SolvedTracePath => {
23
+ const initialTrace = { ...trace, tracePath: simplifyPath(trace.tracePath) }
24
+
25
+ if (trace.globalConnNetId === label.globalConnNetId) {
26
+ return initialTrace
27
+ }
28
+
29
+ const obstacles = getObstacleRects(problem)
30
+ const labelPadding = paddingBuffer
31
+ const labelBoundsRaw = getRectBounds(label.center, label.width, label.height)
32
+ const labelBounds = {
33
+ minX: labelBoundsRaw.minX - labelPadding,
34
+ minY: labelBoundsRaw.minY - labelPadding,
35
+ maxX: labelBoundsRaw.maxX + labelPadding,
36
+ maxY: labelBoundsRaw.maxY + labelPadding,
37
+ chipId: `netlabel-${label.netId}`,
38
+ }
39
+
40
+ const fourPointResult = tryFourPointDetour({
41
+ initialTrace,
42
+ label,
43
+ labelBounds,
44
+ obstacles,
45
+ paddingBuffer,
46
+ detourCount,
47
+ })
48
+ if (fourPointResult) {
49
+ initialTrace.tracePath = fourPointResult.tracePath
50
+ }
51
+ const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone(
52
+ initialTrace.tracePath,
53
+ labelBounds,
54
+ )
55
+
56
+ const snipReconnectResult = trySnipAndReconnect({
57
+ initialTrace,
58
+ firstInsideIndex,
59
+ lastInsideIndex,
60
+ labelBounds,
61
+ obstacles,
62
+ })
63
+
64
+ if (snipReconnectResult) {
65
+ return snipReconnectResult
66
+ }
67
+
68
+ if (fourPointResult) {
69
+ return fourPointResult
70
+ }
71
+
72
+ return initialTrace
73
+ }
@@ -0,0 +1,41 @@
1
+ import type { Point } from "graphics-debug"
2
+ import {
3
+ isHorizontal,
4
+ isVertical,
5
+ } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
6
+
7
+ export const simplifyPath = (path: Point[]): Point[] => {
8
+ if (path.length < 3) return path
9
+ const newPath: Point[] = [path[0]]
10
+ for (let i = 1; i < path.length - 1; i++) {
11
+ const p1 = newPath[newPath.length - 1]
12
+ const p2 = path[i]
13
+ const p3 = path[i + 1]
14
+ if (
15
+ (isVertical(p1, p2) && isVertical(p2, p3)) ||
16
+ (isHorizontal(p1, p2) && isHorizontal(p2, p3))
17
+ ) {
18
+ continue
19
+ }
20
+ newPath.push(p2)
21
+ }
22
+ newPath.push(path[path.length - 1])
23
+
24
+ if (newPath.length < 3) return newPath
25
+ const finalPath: Point[] = [newPath[0]]
26
+ for (let i = 1; i < newPath.length - 1; i++) {
27
+ const p1 = finalPath[finalPath.length - 1]
28
+ const p2 = newPath[i]
29
+ const p3 = newPath[i + 1]
30
+ if (
31
+ (isVertical(p1, p2) && isVertical(p2, p3)) ||
32
+ (isHorizontal(p1, p2) && isHorizontal(p2, p3))
33
+ ) {
34
+ continue
35
+ }
36
+ finalPath.push(p2)
37
+ }
38
+ finalPath.push(newPath[newPath.length - 1])
39
+
40
+ return finalPath
41
+ }
@@ -0,0 +1,14 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+
3
+ export const tryConnectPoints = (start: Point, end: Point): Point[][] => {
4
+ const candidates: Point[][] = []
5
+
6
+ if (start.x === end.x || start.y === end.y) {
7
+ candidates.push([start, end])
8
+ } else {
9
+ candidates.push([start, { x: end.x, y: start.y }, end])
10
+ candidates.push([start, { x: start.x, y: end.y }, end])
11
+ }
12
+
13
+ return candidates
14
+ }
@@ -0,0 +1,229 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { getRectBounds } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
3
+ import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+ import {
5
+ isPathCollidingWithObstacles,
6
+ isVertical,
7
+ segmentIntersectsRect,
8
+ } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
9
+ import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
10
+ import { simplifyPath } from "./simplifyPath"
11
+
12
+ export const trySnipAndReconnect = ({
13
+ initialTrace,
14
+ firstInsideIndex,
15
+ lastInsideIndex,
16
+ labelBounds,
17
+ obstacles,
18
+ }: {
19
+ initialTrace: SolvedTracePath
20
+ firstInsideIndex: number
21
+ lastInsideIndex: number
22
+ labelBounds: any
23
+ obstacles: any[]
24
+ }): SolvedTracePath | null => {
25
+ if (
26
+ firstInsideIndex <= 0 ||
27
+ lastInsideIndex >= initialTrace.tracePath.length - 1
28
+ ) {
29
+ return null
30
+ }
31
+
32
+ const entryPoint = initialTrace.tracePath[firstInsideIndex - 1]
33
+ const exitPoint = initialTrace.tracePath[lastInsideIndex + 1]
34
+
35
+ const pathToEntry = initialTrace.tracePath.slice(0, firstInsideIndex)
36
+ const pathFromExit = initialTrace.tracePath.slice(lastInsideIndex + 1)
37
+
38
+ const candidateDetours: Point[][] = []
39
+
40
+ if (entryPoint.x !== exitPoint.x && entryPoint.y !== exitPoint.y) {
41
+ candidateDetours.push([{ x: exitPoint.x, y: entryPoint.y }])
42
+ candidateDetours.push([{ x: entryPoint.x, y: exitPoint.y }])
43
+ } else if (entryPoint.x === exitPoint.x || entryPoint.y === exitPoint.y) {
44
+ const newPath = [...pathToEntry, ...pathFromExit]
45
+ const simplified = simplifyPath(newPath)
46
+ if (!isPathCollidingWithObstacles(simplified, obstacles)) {
47
+ return { ...initialTrace, tracePath: simplified }
48
+ }
49
+ }
50
+
51
+ for (let i = 0; i < candidateDetours.length; i++) {
52
+ const detour = candidateDetours[i]
53
+ const newFullPath = [...pathToEntry, ...detour, ...pathFromExit]
54
+ const simplified = simplifyPath(newFullPath)
55
+
56
+ if (!isPathCollidingWithObstacles(simplified, obstacles)) {
57
+ return { ...initialTrace, tracePath: simplified }
58
+ }
59
+ }
60
+
61
+ candidateDetours.length = 0
62
+
63
+ const buffer = 0.1
64
+ const leftX = labelBounds.minX - buffer
65
+ const rightX = labelBounds.maxX + buffer
66
+ const topY = labelBounds.maxY + buffer
67
+ const bottomY = labelBounds.minY - buffer
68
+
69
+ if (
70
+ (entryPoint.x <= labelBounds.minX || exitPoint.x <= labelBounds.minX) &&
71
+ entryPoint.x < labelBounds.maxX &&
72
+ exitPoint.x < labelBounds.maxX
73
+ ) {
74
+ candidateDetours.push([
75
+ { x: leftX, y: entryPoint.y },
76
+ { x: leftX, y: exitPoint.y },
77
+ ])
78
+ }
79
+
80
+ if (
81
+ (entryPoint.x >= labelBounds.maxX || exitPoint.x >= labelBounds.maxX) &&
82
+ entryPoint.x > labelBounds.minX &&
83
+ exitPoint.x > labelBounds.minX
84
+ ) {
85
+ candidateDetours.push([
86
+ { x: rightX, y: entryPoint.y },
87
+ { x: rightX, y: exitPoint.y },
88
+ ])
89
+ }
90
+
91
+ if (
92
+ (entryPoint.y >= labelBounds.maxY || exitPoint.y >= labelBounds.maxY) &&
93
+ entryPoint.y > labelBounds.minY &&
94
+ exitPoint.y > labelBounds.minY
95
+ ) {
96
+ candidateDetours.push([
97
+ { x: entryPoint.x, y: topY },
98
+ { x: exitPoint.x, y: topY },
99
+ ])
100
+ }
101
+
102
+ if (
103
+ (entryPoint.y <= labelBounds.minY || exitPoint.y <= labelBounds.minY) &&
104
+ entryPoint.y < labelBounds.maxY &&
105
+ exitPoint.y < labelBounds.maxY
106
+ ) {
107
+ candidateDetours.push([
108
+ { x: entryPoint.x, y: bottomY },
109
+ { x: exitPoint.x, y: bottomY },
110
+ ])
111
+ }
112
+
113
+ for (let i = 0; i < candidateDetours.length; i++) {
114
+ const detour = candidateDetours[i]
115
+ const newFullPath = [...pathToEntry, ...detour, ...pathFromExit]
116
+ const simplified = simplifyPath(newFullPath)
117
+
118
+ if (!isPathCollidingWithObstacles(simplified, obstacles)) {
119
+ return { ...initialTrace, tracePath: simplified }
120
+ }
121
+ }
122
+
123
+ return null
124
+ }
125
+
126
+ export const tryFourPointDetour = ({
127
+ initialTrace,
128
+ label,
129
+ labelBounds,
130
+ obstacles,
131
+ paddingBuffer,
132
+ detourCount,
133
+ }: {
134
+ initialTrace: SolvedTracePath
135
+ label: NetLabelPlacement
136
+ labelBounds: any
137
+ obstacles: any[]
138
+ paddingBuffer: number
139
+ detourCount: number
140
+ }): SolvedTracePath | null => {
141
+ let collidingSegIndex = -1
142
+ for (let i = 0; i < initialTrace.tracePath.length - 1; i++) {
143
+ if (
144
+ segmentIntersectsRect(
145
+ initialTrace.tracePath[i],
146
+ initialTrace.tracePath[i + 1],
147
+ labelBounds,
148
+ )
149
+ ) {
150
+ collidingSegIndex = i
151
+ break
152
+ }
153
+ }
154
+
155
+ if (collidingSegIndex === -1) return initialTrace
156
+
157
+ const pA = initialTrace.tracePath[collidingSegIndex]
158
+ const pB = initialTrace.tracePath[collidingSegIndex + 1]
159
+
160
+ if (!pA || !pB) return null
161
+
162
+ const candidateDetours: Point[][] = []
163
+ const paddedLabelBounds = getRectBounds(
164
+ label.center,
165
+ label.width,
166
+ label.height,
167
+ )
168
+
169
+ const effectivePadding = paddingBuffer + detourCount * paddingBuffer
170
+
171
+ if (isVertical(pA, pB)) {
172
+ const xCandidates = [
173
+ paddedLabelBounds.maxX + effectivePadding,
174
+ paddedLabelBounds.minX - effectivePadding,
175
+ ]
176
+ for (const newX of xCandidates) {
177
+ candidateDetours.push(
178
+ pB.y > pA.y
179
+ ? [
180
+ { x: pA.x, y: paddedLabelBounds.minY - effectivePadding },
181
+ { x: newX, y: paddedLabelBounds.minY - effectivePadding },
182
+ { x: newX, y: paddedLabelBounds.maxY + effectivePadding },
183
+ { x: pB.x, y: paddedLabelBounds.maxY + effectivePadding },
184
+ ]
185
+ : [
186
+ { x: pA.x, y: paddedLabelBounds.maxY + effectivePadding },
187
+ { x: newX, y: paddedLabelBounds.maxY + effectivePadding },
188
+ { x: newX, y: paddedLabelBounds.minY - effectivePadding },
189
+ { x: pB.x, y: paddedLabelBounds.minY - effectivePadding },
190
+ ],
191
+ )
192
+ }
193
+ } else {
194
+ const yCandidates = [
195
+ paddedLabelBounds.maxY + effectivePadding,
196
+ paddedLabelBounds.minY - effectivePadding,
197
+ ]
198
+ for (const newY of yCandidates) {
199
+ candidateDetours.push(
200
+ pB.x > pA.x
201
+ ? [
202
+ { x: paddedLabelBounds.minX - effectivePadding, y: pA.y },
203
+ { x: paddedLabelBounds.minX - effectivePadding, y: newY },
204
+ { x: paddedLabelBounds.maxX + effectivePadding, y: newY },
205
+ { x: paddedLabelBounds.maxX + effectivePadding, y: pB.y },
206
+ ]
207
+ : [
208
+ { x: paddedLabelBounds.maxX + effectivePadding, y: pA.y },
209
+ { x: paddedLabelBounds.maxX + effectivePadding, y: newY },
210
+ { x: paddedLabelBounds.minX - effectivePadding, y: newY },
211
+ { x: paddedLabelBounds.minX - effectivePadding, y: pB.y },
212
+ ],
213
+ )
214
+ }
215
+ }
216
+
217
+ for (const detourPoints of candidateDetours) {
218
+ const finalPath = [
219
+ ...initialTrace.tracePath.slice(0, collidingSegIndex + 1),
220
+ ...detourPoints,
221
+ ...initialTrace.tracePath.slice(collidingSegIndex + 1),
222
+ ]
223
+ const simplifiedFinalPath = simplifyPath(finalPath)
224
+ if (!isPathCollidingWithObstacles(simplifiedFinalPath, obstacles)) {
225
+ return { ...initialTrace, tracePath: simplifiedFinalPath }
226
+ }
227
+ }
228
+ return null
229
+ }
@@ -0,0 +1,211 @@
1
+ import type { Point } from "graphics-debug"
2
+ import { hasCollisions } from "./hasCollisions"
3
+ import { countTurns } from "./countTurns"
4
+ import { simplifyPath } from "./simplifyPath"
5
+ import { tryConnectPoints } from "./tryConnectPoints"
6
+ import { hasCollisionsWithLabels } from "./hasCollisionsWithLabels"
7
+
8
+ export const minimizeTurns = ({
9
+ path,
10
+ obstacles,
11
+ labelBounds,
12
+ }: {
13
+ path: Point[]
14
+ obstacles: any[]
15
+ labelBounds: any[]
16
+ }): Point[] => {
17
+ if (path.length <= 2) {
18
+ return path
19
+ }
20
+
21
+ const recognizeStairStepPattern = (
22
+ pathToCheck: Point[],
23
+ startIdx: number,
24
+ ): number => {
25
+ if (startIdx >= pathToCheck.length - 3) return -1
26
+
27
+ let endIdx = startIdx
28
+ let isStairStep = true
29
+
30
+ for (
31
+ let i = startIdx;
32
+ i < pathToCheck.length - 2 && i < startIdx + 10;
33
+ i++
34
+ ) {
35
+ if (i + 2 >= pathToCheck.length) break
36
+
37
+ const p1 = pathToCheck[i]
38
+ const p2 = pathToCheck[i + 1]
39
+ const p3 = pathToCheck[i + 2]
40
+
41
+ const seg1Vertical = p1.x === p2.x
42
+ const seg2Vertical = p2.x === p3.x
43
+
44
+ if (seg1Vertical === seg2Vertical) {
45
+ break
46
+ }
47
+
48
+ const seg1Direction = seg1Vertical
49
+ ? Math.sign(p2.y - p1.y)
50
+ : Math.sign(p2.x - p1.x)
51
+
52
+ if (i > startIdx) {
53
+ const prevP = pathToCheck[i - 1]
54
+ const prevSegVertical = prevP.x === p1.x
55
+ const prevDirection = prevSegVertical
56
+ ? Math.sign(p1.y - prevP.y)
57
+ : Math.sign(p1.x - prevP.x)
58
+
59
+ if (
60
+ (seg1Vertical &&
61
+ prevSegVertical &&
62
+ seg1Direction !== prevDirection) ||
63
+ (!seg1Vertical && !prevSegVertical && seg1Direction !== prevDirection)
64
+ ) {
65
+ isStairStep = false
66
+ break
67
+ }
68
+ }
69
+
70
+ endIdx = i + 2
71
+ }
72
+
73
+ return isStairStep && endIdx - startIdx >= 3 ? endIdx : -1
74
+ }
75
+
76
+ let optimizedPath = [...path]
77
+ let currentTurns = countTurns(optimizedPath)
78
+ let improved = true
79
+
80
+ while (improved) {
81
+ improved = false
82
+
83
+ // First try to identify and replace stair-step patterns
84
+ for (let startIdx = 0; startIdx < optimizedPath.length - 3; startIdx++) {
85
+ const stairEndIdx = recognizeStairStepPattern(optimizedPath, startIdx)
86
+
87
+ if (stairEndIdx > 0) {
88
+ const startPoint = optimizedPath[startIdx]
89
+ const endPoint = optimizedPath[stairEndIdx]
90
+
91
+ const connectionOptions = tryConnectPoints(startPoint, endPoint)
92
+
93
+ for (const connection of connectionOptions) {
94
+ const testPath = [
95
+ ...optimizedPath.slice(0, startIdx + 1),
96
+ ...connection.slice(1, -1),
97
+ ...optimizedPath.slice(stairEndIdx),
98
+ ]
99
+
100
+ const collidesWithObstacles = hasCollisions(connection, obstacles)
101
+ const collidesWithLabels = hasCollisionsWithLabels(
102
+ connection,
103
+ labelBounds,
104
+ )
105
+
106
+ if (!collidesWithObstacles && !collidesWithLabels) {
107
+ const newTurns = countTurns(testPath)
108
+ const turnsRemoved = stairEndIdx - startIdx - 1
109
+
110
+ optimizedPath = testPath
111
+ currentTurns = newTurns
112
+ improved = true
113
+ break
114
+ }
115
+ }
116
+
117
+ if (improved) break
118
+ }
119
+ }
120
+
121
+ // If no stair-step optimization worked, try regular point removal
122
+ if (!improved) {
123
+ for (let startIdx = 0; startIdx < optimizedPath.length - 2; startIdx++) {
124
+ const maxRemove = Math.min(
125
+ optimizedPath.length - startIdx - 2,
126
+ optimizedPath.length - 2,
127
+ )
128
+
129
+ for (let removeCount = 1; removeCount <= maxRemove; removeCount++) {
130
+ const endIdx = startIdx + removeCount + 1
131
+
132
+ if (endIdx >= optimizedPath.length) continue
133
+
134
+ const startPoint = optimizedPath[startIdx]
135
+ const endPoint = optimizedPath[endIdx]
136
+
137
+ const connectionOptions = tryConnectPoints(startPoint, endPoint)
138
+
139
+ for (const connection of connectionOptions) {
140
+ const testPath = [
141
+ ...optimizedPath.slice(0, startIdx + 1),
142
+ ...connection.slice(1, -1),
143
+ ...optimizedPath.slice(endIdx),
144
+ ]
145
+
146
+ const connectionSegments = connection
147
+ const collidesWithObstacles = hasCollisions(
148
+ connectionSegments,
149
+ obstacles,
150
+ )
151
+ const collidesWithLabels = hasCollisionsWithLabels(
152
+ connectionSegments,
153
+ labelBounds,
154
+ )
155
+
156
+ if (!collidesWithObstacles && !collidesWithLabels) {
157
+ const newTurns = countTurns(testPath)
158
+
159
+ if (
160
+ newTurns < currentTurns ||
161
+ (newTurns === currentTurns &&
162
+ testPath.length < optimizedPath.length)
163
+ ) {
164
+ optimizedPath = testPath
165
+ currentTurns = newTurns
166
+ improved = true
167
+ break
168
+ }
169
+ }
170
+ }
171
+
172
+ if (improved) break
173
+ }
174
+ if (improved) break
175
+ }
176
+ }
177
+
178
+ if (!improved) {
179
+ for (let i = 0; i < optimizedPath.length - 2; i++) {
180
+ const p1 = optimizedPath[i]
181
+ const p2 = optimizedPath[i + 1]
182
+ const p3 = optimizedPath[i + 2]
183
+
184
+ const allVertical = p1.x === p2.x && p2.x === p3.x
185
+ const allHorizontal = p1.y === p2.y && p2.y === p3.y
186
+
187
+ if (allVertical || allHorizontal) {
188
+ const testPath = [
189
+ ...optimizedPath.slice(0, i + 1),
190
+ ...optimizedPath.slice(i + 2),
191
+ ]
192
+
193
+ const collidesWithObstacles = hasCollisions([p1, p3], obstacles)
194
+ const collidesWithLabels = hasCollisionsWithLabels(
195
+ [p1, p3],
196
+ labelBounds,
197
+ )
198
+
199
+ if (!collidesWithObstacles && !collidesWithLabels) {
200
+ optimizedPath = testPath
201
+ improved = true
202
+ break
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ const finalSimplifiedPath = simplifyPath(optimizedPath)
210
+ return finalSimplifiedPath
211
+ }
@@ -0,0 +1,25 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+
3
+ export const findTraceViolationZone = (
4
+ path: Point[],
5
+ labelBounds: { minX: number; maxX: number; minY: number; maxY: number },
6
+ ) => {
7
+ const isPointInside = (p: Point) =>
8
+ p.x > labelBounds.minX &&
9
+ p.x < labelBounds.maxX &&
10
+ p.y > labelBounds.minY &&
11
+ p.y < labelBounds.maxY
12
+
13
+ let firstInsideIndex = -1
14
+ let lastInsideIndex = -1
15
+
16
+ for (let i = 0; i < path.length; i++) {
17
+ if (isPointInside(path[i])) {
18
+ if (firstInsideIndex === -1) {
19
+ firstInsideIndex = i
20
+ }
21
+ lastInsideIndex = i
22
+ }
23
+ }
24
+ return { firstInsideIndex, lastInsideIndex }
25
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.37",
4
+ "version": "0.0.39",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",