@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.
- package/dist/index.d.ts +25 -1
- package/dist/index.js +917 -10
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions.ts +17 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +31 -12
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +40 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +240 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/balanceLShapes.ts +207 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/countTurns.ts +18 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap.ts +38 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/hasCollisions.ts +22 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/hasCollisionsWithLabels.ts +19 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/index.ts +1 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/minimizeTurnsWithFilteredLabels.ts +66 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +73 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/simplifyPath.ts +41 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryConnectPoints.ts +14 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts +229 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/turnMinimization.ts +211 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/violation.ts +25 -0
- package/package.json +1 -1
- package/site/examples/example20.page.tsx +103 -0
- package/site/examples/example22.page.tsx +108 -0
- package/site/examples/example23.page.tsx +137 -0
- package/site/examples/example24.page.tsx +128 -0
- package/tests/examples/__snapshots__/example16.snap.svg +27 -27
- package/tests/examples/__snapshots__/example22.snap.svg +137 -0
- package/tests/examples/__snapshots__/example23.snap.svg +137 -0
- package/tests/examples/__snapshots__/example24.snap.svg +143 -0
- package/tests/examples/__snapshots__/example25.snap.svg +165 -0
- package/tests/examples/__snapshots__/example26.snap.svg +157 -0
- package/tests/examples/example22.test.tsx +109 -0
- package/tests/examples/example23.test.tsx +109 -0
- package/tests/examples/example24.test.tsx +114 -0
- package/tests/examples/example25.test.tsx +143 -0
- 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
|
+
}
|