@tscircuit/curvy-trace-solver 0.0.3 → 0.0.4
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/package.json +4 -1
- package/.github/workflows/bun-formatcheck.yml +0 -26
- package/.github/workflows/bun-pver-release.yml +0 -92
- package/.github/workflows/bun-test.yml +0 -40
- package/.github/workflows/bun-typecheck.yml +0 -26
- package/biome.json +0 -96
- package/bunfig.toml +0 -5
- package/cosmos.config.json +0 -5
- package/cosmos.decorator.tsx +0 -21
- package/fixtures/basics/basics01-input.json +0 -15
- package/fixtures/basics/basics01.fixture.tsx +0 -11
- package/fixtures/problem-generator.fixture.tsx +0 -50
- package/index.html +0 -12
- package/lib/CurvyTraceSolver.ts +0 -775
- package/lib/geometry/getObstacleOuterSegments.ts +0 -25
- package/lib/geometry/index.ts +0 -8
- package/lib/index.ts +0 -2
- package/lib/problem-generator/countChordCrossings.ts +0 -119
- package/lib/problem-generator/createRng.ts +0 -13
- package/lib/problem-generator/generateRandomProblem.ts +0 -225
- package/lib/problem-generator/index.ts +0 -1
- package/lib/problem-generator/randomBoundaryPoint.ts +0 -30
- package/lib/problem-generator/wouldCrossAny.ts +0 -16
- package/lib/sampleTraceIntoSegments.ts +0 -62
- package/lib/scoreOutputCost.ts +0 -116
- package/lib/types.ts +0 -35
- package/lib/visualization-utils/index.ts +0 -14
- package/lib/visualization-utils/visualizeCurvyTraceProblem.ts +0 -66
- package/scripts/benchmarks/run-benchmark.ts +0 -85
- package/tests/__snapshots__/svg.snap.svg +0 -3
- package/tests/basics/__snapshots__/basics01.snap.svg +0 -44
- package/tests/basics/basics01.test.ts +0 -12
- package/tests/fixtures/preload.ts +0 -1
- package/tests/svg.test.ts +0 -12
- package/tsconfig.json +0 -35
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
-
import type { Obstacle } from "lib/types"
|
|
3
|
-
|
|
4
|
-
export const getObstacleOuterSegments = (obstacle: Obstacle) => {
|
|
5
|
-
const segments: [Point, Point][] = [
|
|
6
|
-
[
|
|
7
|
-
{ x: obstacle.minX, y: obstacle.minY },
|
|
8
|
-
{ x: obstacle.maxX, y: obstacle.minY },
|
|
9
|
-
],
|
|
10
|
-
[
|
|
11
|
-
{ x: obstacle.maxX, y: obstacle.minY },
|
|
12
|
-
{ x: obstacle.maxX, y: obstacle.maxY },
|
|
13
|
-
],
|
|
14
|
-
[
|
|
15
|
-
{ x: obstacle.maxX, y: obstacle.maxY },
|
|
16
|
-
{ x: obstacle.minX, y: obstacle.maxY },
|
|
17
|
-
],
|
|
18
|
-
[
|
|
19
|
-
{ x: obstacle.minX, y: obstacle.maxY },
|
|
20
|
-
{ x: obstacle.minX, y: obstacle.minY },
|
|
21
|
-
],
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
return segments
|
|
25
|
-
}
|
package/lib/geometry/index.ts
DELETED
package/lib/index.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import type { WaypointPair } from "lib/types"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Maps a boundary point to a 1D perimeter coordinate.
|
|
5
|
-
* Starting at top-left corner, going clockwise:
|
|
6
|
-
* - Top edge (y=ymax): t = x - xmin
|
|
7
|
-
* - Right edge (x=xmax): t = W + (ymax - y)
|
|
8
|
-
* - Bottom edge (y=ymin): t = 2W + H + (xmax - x)
|
|
9
|
-
* - Left edge (x=xmin): t = 2W + 2H + (y - ymin)
|
|
10
|
-
*/
|
|
11
|
-
export function perimeterT(
|
|
12
|
-
p: { x: number; y: number },
|
|
13
|
-
xmin: number,
|
|
14
|
-
xmax: number,
|
|
15
|
-
ymin: number,
|
|
16
|
-
ymax: number,
|
|
17
|
-
): number {
|
|
18
|
-
const W = xmax - xmin
|
|
19
|
-
const H = ymax - ymin
|
|
20
|
-
const eps = 1e-6
|
|
21
|
-
|
|
22
|
-
// Top edge
|
|
23
|
-
if (Math.abs(p.y - ymax) < eps) {
|
|
24
|
-
return p.x - xmin
|
|
25
|
-
}
|
|
26
|
-
// Right edge
|
|
27
|
-
if (Math.abs(p.x - xmax) < eps) {
|
|
28
|
-
return W + (ymax - p.y)
|
|
29
|
-
}
|
|
30
|
-
// Bottom edge
|
|
31
|
-
if (Math.abs(p.y - ymin) < eps) {
|
|
32
|
-
return W + H + (xmax - p.x)
|
|
33
|
-
}
|
|
34
|
-
// Left edge
|
|
35
|
-
if (Math.abs(p.x - xmin) < eps) {
|
|
36
|
-
return 2 * W + H + (p.y - ymin)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Point is not exactly on boundary - find closest edge
|
|
40
|
-
const distTop = Math.abs(p.y - ymax)
|
|
41
|
-
const distRight = Math.abs(p.x - xmax)
|
|
42
|
-
const distBottom = Math.abs(p.y - ymin)
|
|
43
|
-
const distLeft = Math.abs(p.x - xmin)
|
|
44
|
-
|
|
45
|
-
const minDist = Math.min(distTop, distRight, distBottom, distLeft)
|
|
46
|
-
|
|
47
|
-
if (minDist === distTop) {
|
|
48
|
-
return Math.max(0, Math.min(W, p.x - xmin))
|
|
49
|
-
}
|
|
50
|
-
if (minDist === distRight) {
|
|
51
|
-
return W + Math.max(0, Math.min(H, ymax - p.y))
|
|
52
|
-
}
|
|
53
|
-
if (minDist === distBottom) {
|
|
54
|
-
return W + H + Math.max(0, Math.min(W, xmax - p.x))
|
|
55
|
-
}
|
|
56
|
-
// Left edge
|
|
57
|
-
return 2 * W + H + Math.max(0, Math.min(H, p.y - ymin))
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Check if two perimeter coordinates are coincident (within epsilon)
|
|
62
|
-
*/
|
|
63
|
-
function areCoincident(t1: number, t2: number, eps: number = 1e-6): boolean {
|
|
64
|
-
return Math.abs(t1 - t2) < eps
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Check if two chords cross using the interleaving criterion.
|
|
69
|
-
* Two chords (a,b) and (c,d) with a < b and c < d cross iff: a < c < b < d OR c < a < d < b
|
|
70
|
-
*
|
|
71
|
-
* Chords that share a coincident endpoint do NOT count as crossing.
|
|
72
|
-
*/
|
|
73
|
-
export function chordsCross(
|
|
74
|
-
chord1: [number, number],
|
|
75
|
-
chord2: [number, number],
|
|
76
|
-
): boolean {
|
|
77
|
-
// Normalize each chord so first endpoint is smaller
|
|
78
|
-
const [a, b] = chord1[0] < chord1[1] ? chord1 : [chord1[1], chord1[0]]
|
|
79
|
-
const [c, d] = chord2[0] < chord2[1] ? chord2 : [chord2[1], chord2[0]]
|
|
80
|
-
|
|
81
|
-
// Skip if chords share a coincident endpoint
|
|
82
|
-
if (
|
|
83
|
-
areCoincident(a, c) ||
|
|
84
|
-
areCoincident(a, d) ||
|
|
85
|
-
areCoincident(b, c) ||
|
|
86
|
-
areCoincident(b, d)
|
|
87
|
-
) {
|
|
88
|
-
return false
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Two chords cross iff their endpoints interleave: a < c < b < d OR c < a < d < b
|
|
92
|
-
return (a < c && c < b && b < d) || (c < a && a < d && d < b)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const countChordCrossings = (
|
|
96
|
-
waypointPairs: WaypointPair[],
|
|
97
|
-
bounds: { minX: number; maxX: number; minY: number; maxY: number },
|
|
98
|
-
): number => {
|
|
99
|
-
const { minX: xmin, maxX: xmax, minY: ymin, maxY: ymax } = bounds
|
|
100
|
-
|
|
101
|
-
// Map all waypoint pairs to perimeter coordinates (t)
|
|
102
|
-
const chords: [number, number][] = waypointPairs.map((pair) => {
|
|
103
|
-
const t1 = perimeterT(pair.start, xmin, xmax, ymin, ymax)
|
|
104
|
-
const t2 = perimeterT(pair.end, xmin, xmax, ymin, ymax)
|
|
105
|
-
return [t1, t2]
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
// Count all crossings between unique pairs
|
|
109
|
-
let crossings = 0
|
|
110
|
-
for (let i = 0; i < chords.length; i++) {
|
|
111
|
-
for (let j = i + 1; j < chords.length; j++) {
|
|
112
|
-
if (chordsCross(chords[i], chords[j])) {
|
|
113
|
-
crossings++
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return crossings
|
|
119
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple seeded random number generator (mulberry32)
|
|
3
|
-
*/
|
|
4
|
-
export function createRng(seed: number) {
|
|
5
|
-
let state = seed
|
|
6
|
-
return () => {
|
|
7
|
-
state |= 0
|
|
8
|
-
state = (state + 0x6d2b79f5) | 0
|
|
9
|
-
let t = Math.imul(state ^ (state >>> 15), 1 | state)
|
|
10
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
11
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
12
|
-
}
|
|
13
|
-
}
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import type { Bounds } from "@tscircuit/math-utils"
|
|
2
|
-
import type { CurvyTraceProblem, Obstacle, WaypointPair } from "lib/types"
|
|
3
|
-
import { perimeterT } from "./countChordCrossings"
|
|
4
|
-
import { createRng } from "./createRng"
|
|
5
|
-
import { randomBoundaryPoint } from "./randomBoundaryPoint"
|
|
6
|
-
import { wouldCrossAny } from "./wouldCrossAny"
|
|
7
|
-
|
|
8
|
-
type Side = "top" | "bottom" | "left" | "right"
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Check if two obstacles overlap.
|
|
12
|
-
*/
|
|
13
|
-
const obstaclesOverlap = (a: Obstacle, b: Obstacle): boolean => {
|
|
14
|
-
return !(
|
|
15
|
-
a.maxX <= b.minX ||
|
|
16
|
-
a.minX >= b.maxX ||
|
|
17
|
-
a.maxY <= b.minY ||
|
|
18
|
-
a.minY >= b.maxY
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Generate an obstacle that is outside the bounds but touching a specific side.
|
|
24
|
-
* The obstacle takes up at least 10% of the side it's touching.
|
|
25
|
-
*/
|
|
26
|
-
const generateObstacleOnSide = (
|
|
27
|
-
rng: () => number,
|
|
28
|
-
bounds: Bounds,
|
|
29
|
-
side: Side,
|
|
30
|
-
): Obstacle => {
|
|
31
|
-
const { minX, maxX, minY, maxY } = bounds
|
|
32
|
-
const W = maxX - minX
|
|
33
|
-
const H = maxY - minY
|
|
34
|
-
|
|
35
|
-
// Obstacle depth (how far it extends outside the bounds)
|
|
36
|
-
const minDepth = Math.min(W, H) * 0.1
|
|
37
|
-
const maxDepth = Math.min(W, H) * 0.3
|
|
38
|
-
const depth = minDepth + rng() * (maxDepth - minDepth)
|
|
39
|
-
|
|
40
|
-
let obstacleMinX: number
|
|
41
|
-
let obstacleMaxX: number
|
|
42
|
-
let obstacleMinY: number
|
|
43
|
-
let obstacleMaxY: number
|
|
44
|
-
|
|
45
|
-
if (side === "top" || side === "bottom") {
|
|
46
|
-
// Obstacle spans horizontally along top or bottom
|
|
47
|
-
const minSpan = W * 0.1 // At least 10% of the side
|
|
48
|
-
const maxSpan = W * 0.4 // Up to 40% of the side
|
|
49
|
-
const span = minSpan + rng() * (maxSpan - minSpan)
|
|
50
|
-
|
|
51
|
-
// Random position along the side (ensuring it fits within bounds)
|
|
52
|
-
const startX = minX + rng() * (W - span)
|
|
53
|
-
obstacleMinX = startX
|
|
54
|
-
obstacleMaxX = startX + span
|
|
55
|
-
|
|
56
|
-
if (side === "top") {
|
|
57
|
-
obstacleMinY = maxY
|
|
58
|
-
obstacleMaxY = maxY + depth
|
|
59
|
-
} else {
|
|
60
|
-
obstacleMinY = minY - depth
|
|
61
|
-
obstacleMaxY = minY
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
// Obstacle spans vertically along left or right
|
|
65
|
-
const minSpan = H * 0.1 // At least 10% of the side
|
|
66
|
-
const maxSpan = H * 0.4 // Up to 40% of the side
|
|
67
|
-
const span = minSpan + rng() * (maxSpan - minSpan)
|
|
68
|
-
|
|
69
|
-
// Random position along the side (ensuring it fits within bounds)
|
|
70
|
-
const startY = minY + rng() * (H - span)
|
|
71
|
-
obstacleMinY = startY
|
|
72
|
-
obstacleMaxY = startY + span
|
|
73
|
-
|
|
74
|
-
if (side === "right") {
|
|
75
|
-
obstacleMinX = maxX
|
|
76
|
-
obstacleMaxX = maxX + depth
|
|
77
|
-
} else {
|
|
78
|
-
obstacleMinX = minX - depth
|
|
79
|
-
obstacleMaxX = minX
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
minX: obstacleMinX,
|
|
85
|
-
maxX: obstacleMaxX,
|
|
86
|
-
minY: obstacleMinY,
|
|
87
|
-
maxY: obstacleMaxY,
|
|
88
|
-
center: {
|
|
89
|
-
x: (obstacleMinX + obstacleMaxX) / 2,
|
|
90
|
-
y: (obstacleMinY + obstacleMaxY) / 2,
|
|
91
|
-
},
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const generateRandomProblem = (opts: {
|
|
96
|
-
numWaypointPairs: number
|
|
97
|
-
numObstacles: number
|
|
98
|
-
randomSeed: number
|
|
99
|
-
bounds?: Bounds
|
|
100
|
-
preferredSpacing?: number
|
|
101
|
-
/**
|
|
102
|
-
* Minimum spacing between a waypoint point and any other waypoint point.
|
|
103
|
-
*/
|
|
104
|
-
minSpacing?: number
|
|
105
|
-
}): CurvyTraceProblem => {
|
|
106
|
-
|
|
107
|
-
const rng = createRng(opts.randomSeed)
|
|
108
|
-
|
|
109
|
-
const bounds = opts.bounds ?? { minX: 0, maxX: 100, minY: 0, maxY: 100 }
|
|
110
|
-
const { minX: xmin, maxX: xmax, minY: ymin, maxY: ymax } = bounds
|
|
111
|
-
const W = xmax - xmin
|
|
112
|
-
const H = ymax - ymin
|
|
113
|
-
const perimeter = 2 * W + 2 * H
|
|
114
|
-
|
|
115
|
-
const waypointPairs: WaypointPair[] = []
|
|
116
|
-
const chords: [number, number][] = []
|
|
117
|
-
|
|
118
|
-
const MAX_ATTEMPTS = 1000
|
|
119
|
-
|
|
120
|
-
for (let i = 0; i < opts.numWaypointPairs; i++) {
|
|
121
|
-
let attempts = 0
|
|
122
|
-
let start: { x: number; y: number } | null = null
|
|
123
|
-
let end: { x: number; y: number } | null = null
|
|
124
|
-
let newChord: [number, number] | null = null
|
|
125
|
-
|
|
126
|
-
while (attempts < MAX_ATTEMPTS) {
|
|
127
|
-
// Generate two random boundary points
|
|
128
|
-
start = randomBoundaryPoint(rng, xmin, xmax, ymin, ymax)
|
|
129
|
-
end = randomBoundaryPoint(rng, xmin, xmax, ymin, ymax)
|
|
130
|
-
|
|
131
|
-
// Convert to perimeter t values using existing utility
|
|
132
|
-
const t1 = perimeterT(start, xmin, xmax, ymin, ymax)
|
|
133
|
-
const t2 = perimeterT(end, xmin, xmax, ymin, ymax)
|
|
134
|
-
|
|
135
|
-
// Ensure the two points aren't too close together
|
|
136
|
-
const minSeparation = perimeter * 0.05
|
|
137
|
-
const dist = Math.min(Math.abs(t1 - t2), perimeter - Math.abs(t1 - t2))
|
|
138
|
-
if (dist < minSeparation) {
|
|
139
|
-
attempts++
|
|
140
|
-
continue
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
newChord = [t1, t2]
|
|
144
|
-
|
|
145
|
-
// Check minSpacing constraint against all existing waypoint points
|
|
146
|
-
if (opts.minSpacing !== undefined) {
|
|
147
|
-
let tooClose = false
|
|
148
|
-
for (const pair of waypointPairs) {
|
|
149
|
-
const dStart1 = Math.hypot(start.x - pair.start.x, start.y - pair.start.y)
|
|
150
|
-
const dStart2 = Math.hypot(start.x - pair.end.x, start.y - pair.end.y)
|
|
151
|
-
const dEnd1 = Math.hypot(end.x - pair.start.x, end.y - pair.start.y)
|
|
152
|
-
const dEnd2 = Math.hypot(end.x - pair.end.x, end.y - pair.end.y)
|
|
153
|
-
if (dStart1 < opts.minSpacing || dStart2 < opts.minSpacing ||
|
|
154
|
-
dEnd1 < opts.minSpacing || dEnd2 < opts.minSpacing) {
|
|
155
|
-
tooClose = true
|
|
156
|
-
break
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (tooClose) {
|
|
160
|
-
attempts++
|
|
161
|
-
continue
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Check if this chord would cross any existing chords
|
|
166
|
-
if (!wouldCrossAny(newChord, chords)) {
|
|
167
|
-
break
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
newChord = null
|
|
171
|
-
attempts++
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (newChord === null || start === null || end === null) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Failed to generate non-crossing waypoint pair after ${MAX_ATTEMPTS} attempts. ` +
|
|
177
|
-
`This may happen if too many waypoint pairs are requested.`,
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
chords.push(newChord)
|
|
182
|
-
waypointPairs.push({ start, end, networkId: `net${i}` })
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Generate obstacles outside the bounds but touching them
|
|
186
|
-
const obstacles: Obstacle[] = []
|
|
187
|
-
const sides: Side[] = ["top", "bottom", "left", "right"]
|
|
188
|
-
|
|
189
|
-
for (let i = 0; i < opts.numObstacles; i++) {
|
|
190
|
-
let attempts = 0
|
|
191
|
-
let obstacle: Obstacle | null = null
|
|
192
|
-
|
|
193
|
-
while (attempts < MAX_ATTEMPTS) {
|
|
194
|
-
// Pick a random side for this obstacle
|
|
195
|
-
const side = sides[Math.floor(rng() * sides.length)]
|
|
196
|
-
const candidate = generateObstacleOnSide(rng, bounds, side)
|
|
197
|
-
|
|
198
|
-
// Check if it overlaps with any existing obstacle
|
|
199
|
-
const overlaps = obstacles.some((existing) =>
|
|
200
|
-
obstaclesOverlap(candidate, existing),
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
if (!overlaps) {
|
|
204
|
-
obstacle = candidate
|
|
205
|
-
break
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
attempts++
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (obstacle === null) {
|
|
212
|
-
// Could not place obstacle without overlap, skip it
|
|
213
|
-
break
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
obstacles.push(obstacle)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
bounds,
|
|
221
|
-
waypointPairs,
|
|
222
|
-
obstacles,
|
|
223
|
-
preferredSpacing: opts.preferredSpacing ?? 10,
|
|
224
|
-
}
|
|
225
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./generateRandomProblem"
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate a random point on the boundary of the rectangle
|
|
3
|
-
*/
|
|
4
|
-
export function randomBoundaryPoint(
|
|
5
|
-
rng: () => number,
|
|
6
|
-
xmin: number,
|
|
7
|
-
xmax: number,
|
|
8
|
-
ymin: number,
|
|
9
|
-
ymax: number,
|
|
10
|
-
): { x: number; y: number } {
|
|
11
|
-
const W = xmax - xmin
|
|
12
|
-
const H = ymax - ymin
|
|
13
|
-
const perimeter = 2 * W + 2 * H
|
|
14
|
-
const t = rng() * perimeter
|
|
15
|
-
|
|
16
|
-
// Top edge: t in [0, W)
|
|
17
|
-
if (t < W) {
|
|
18
|
-
return { x: xmin + t, y: ymax }
|
|
19
|
-
}
|
|
20
|
-
// Right edge: t in [W, W + H)
|
|
21
|
-
if (t < W + H) {
|
|
22
|
-
return { x: xmax, y: ymax - (t - W) }
|
|
23
|
-
}
|
|
24
|
-
// Bottom edge: t in [W + H, 2W + H)
|
|
25
|
-
if (t < 2 * W + H) {
|
|
26
|
-
return { x: xmax - (t - W - H), y: ymin }
|
|
27
|
-
}
|
|
28
|
-
// Left edge: t in [2W + H, 2W + 2H)
|
|
29
|
-
return { x: xmin, y: ymin + (t - 2 * W - H) }
|
|
30
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { chordsCross } from "./countChordCrossings"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Check if a new chord would cross any existing chords
|
|
5
|
-
*/
|
|
6
|
-
export function wouldCrossAny(
|
|
7
|
-
newChord: [number, number],
|
|
8
|
-
existingChords: [number, number][],
|
|
9
|
-
): boolean {
|
|
10
|
-
for (const existing of existingChords) {
|
|
11
|
-
if (chordsCross(newChord, existing)) {
|
|
12
|
-
return true
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return false
|
|
16
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Samples a trace (array of points) into evenly-spaced segments.
|
|
5
|
-
* This is useful for limiting computation when comparing traces.
|
|
6
|
-
*/
|
|
7
|
-
export const sampleTraceIntoSegments = (
|
|
8
|
-
points: Point[],
|
|
9
|
-
numSegments: number,
|
|
10
|
-
networkId?: string,
|
|
11
|
-
): { segment: [Point, Point]; networkId?: string }[] => {
|
|
12
|
-
if (points.length < 2) return []
|
|
13
|
-
|
|
14
|
-
// Compute total length of the trace
|
|
15
|
-
let totalLength = 0
|
|
16
|
-
for (let i = 0; i < points.length - 1; i++) {
|
|
17
|
-
const dx = points[i + 1].x - points[i].x
|
|
18
|
-
const dy = points[i + 1].y - points[i].y
|
|
19
|
-
totalLength += Math.sqrt(dx * dx + dy * dy)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (totalLength === 0) return []
|
|
23
|
-
|
|
24
|
-
const segmentLength = totalLength / numSegments
|
|
25
|
-
const sampledPoints: Point[] = [points[0]]
|
|
26
|
-
|
|
27
|
-
let currentLength = 0
|
|
28
|
-
let pointIndex = 0
|
|
29
|
-
|
|
30
|
-
for (let i = 1; i <= numSegments; i++) {
|
|
31
|
-
const targetLength = i * segmentLength
|
|
32
|
-
|
|
33
|
-
while (pointIndex < points.length - 1) {
|
|
34
|
-
const dx = points[pointIndex + 1].x - points[pointIndex].x
|
|
35
|
-
const dy = points[pointIndex + 1].y - points[pointIndex].y
|
|
36
|
-
const segLen = Math.sqrt(dx * dx + dy * dy)
|
|
37
|
-
|
|
38
|
-
if (currentLength + segLen >= targetLength) {
|
|
39
|
-
const t = (targetLength - currentLength) / segLen
|
|
40
|
-
sampledPoints.push({
|
|
41
|
-
x: points[pointIndex].x + t * dx,
|
|
42
|
-
y: points[pointIndex].y + t * dy,
|
|
43
|
-
})
|
|
44
|
-
break
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
currentLength += segLen
|
|
48
|
-
pointIndex++
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Convert sampled points to segments
|
|
53
|
-
const segments: { segment: [Point, Point]; networkId?: string }[] = []
|
|
54
|
-
for (let i = 0; i < sampledPoints.length - 1; i++) {
|
|
55
|
-
segments.push({
|
|
56
|
-
segment: [sampledPoints[i], sampledPoints[i + 1]],
|
|
57
|
-
networkId,
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return segments
|
|
62
|
-
}
|
package/lib/scoreOutputCost.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import type { CurvyTraceProblem, OutputTrace } from "./types"
|
|
2
|
-
import type { Point } from "@tscircuit/math-utils"
|
|
3
|
-
import { segmentToSegmentMinDistance } from "@tscircuit/math-utils"
|
|
4
|
-
import { sampleTraceIntoSegments } from "./sampleTraceIntoSegments"
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Returns a cost for a solution, lower is better output
|
|
8
|
-
*
|
|
9
|
-
* The cost is computed by sampling the output traces into samplesPerTrace
|
|
10
|
-
* segments, then computing the minimum segment to segment distance to all
|
|
11
|
-
* other-network segments in the output and obstacles. Beyond
|
|
12
|
-
* problem.preferredSpacing the cost is 0. The max segment-to-segment cost
|
|
13
|
-
* is problem.preferredSpacing**2
|
|
14
|
-
*
|
|
15
|
-
* If a segment intersects any segment from a different net, an additional
|
|
16
|
-
* penalty of samplesPerTrace * preferredSpacing**2 is added.
|
|
17
|
-
*/
|
|
18
|
-
export const scoreOutputCost = ({
|
|
19
|
-
problem,
|
|
20
|
-
outputTraces,
|
|
21
|
-
samplesPerTrace = 20,
|
|
22
|
-
}: {
|
|
23
|
-
problem: CurvyTraceProblem
|
|
24
|
-
outputTraces: OutputTrace[]
|
|
25
|
-
samplesPerTrace?: number
|
|
26
|
-
}): number => {
|
|
27
|
-
const { preferredSpacing, obstacles } = problem
|
|
28
|
-
|
|
29
|
-
// Collect all trace segments
|
|
30
|
-
const allTraceSegments: { segment: [Point, Point]; networkId?: string }[] = []
|
|
31
|
-
for (const trace of outputTraces) {
|
|
32
|
-
const segments = sampleTraceIntoSegments(
|
|
33
|
-
trace.points,
|
|
34
|
-
samplesPerTrace,
|
|
35
|
-
trace.networkId,
|
|
36
|
-
)
|
|
37
|
-
allTraceSegments.push(...segments)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Collect all obstacle segments
|
|
41
|
-
const allObstacleSegments: { segment: [Point, Point]; networkId?: string }[] =
|
|
42
|
-
[]
|
|
43
|
-
for (const obstacle of obstacles) {
|
|
44
|
-
if (obstacle.outerSegments) {
|
|
45
|
-
for (const seg of obstacle.outerSegments) {
|
|
46
|
-
allObstacleSegments.push({
|
|
47
|
-
segment: seg,
|
|
48
|
-
networkId: obstacle.networkId,
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Compute total cost
|
|
55
|
-
let totalCost = 0
|
|
56
|
-
|
|
57
|
-
for (let i = 0; i < allTraceSegments.length; i++) {
|
|
58
|
-
const seg1 = allTraceSegments[i]
|
|
59
|
-
|
|
60
|
-
// Check against other trace segments (different network)
|
|
61
|
-
for (let j = i + 1; j < allTraceSegments.length; j++) {
|
|
62
|
-
const seg2 = allTraceSegments[j]
|
|
63
|
-
|
|
64
|
-
// Skip if same network (traces in same network are allowed to intersect)
|
|
65
|
-
if (
|
|
66
|
-
seg1.networkId &&
|
|
67
|
-
seg2.networkId &&
|
|
68
|
-
seg1.networkId === seg2.networkId
|
|
69
|
-
) {
|
|
70
|
-
continue
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const dist = segmentToSegmentMinDistance(
|
|
74
|
-
seg1.segment[0],
|
|
75
|
-
seg1.segment[1],
|
|
76
|
-
seg2.segment[0],
|
|
77
|
-
seg2.segment[1],
|
|
78
|
-
)
|
|
79
|
-
if (dist < preferredSpacing) {
|
|
80
|
-
totalCost += (preferredSpacing - dist) ** 2
|
|
81
|
-
}
|
|
82
|
-
// Add cross-net intersection penalty
|
|
83
|
-
if (dist < 1e-9) {
|
|
84
|
-
totalCost += samplesPerTrace * preferredSpacing ** 2
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Check against obstacle segments (different network)
|
|
89
|
-
for (const obsSeg of allObstacleSegments) {
|
|
90
|
-
// Skip if same network
|
|
91
|
-
if (
|
|
92
|
-
seg1.networkId &&
|
|
93
|
-
obsSeg.networkId &&
|
|
94
|
-
seg1.networkId === obsSeg.networkId
|
|
95
|
-
) {
|
|
96
|
-
continue
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const dist = segmentToSegmentMinDistance(
|
|
100
|
-
seg1.segment[0],
|
|
101
|
-
seg1.segment[1],
|
|
102
|
-
obsSeg.segment[0],
|
|
103
|
-
obsSeg.segment[1],
|
|
104
|
-
)
|
|
105
|
-
if (dist < preferredSpacing) {
|
|
106
|
-
totalCost += (preferredSpacing - dist) ** 2
|
|
107
|
-
}
|
|
108
|
-
// Add cross-net intersection penalty
|
|
109
|
-
if (dist < 1e-9) {
|
|
110
|
-
totalCost += samplesPerTrace * preferredSpacing ** 2
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return totalCost
|
|
116
|
-
}
|
package/lib/types.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { Point, Bounds } from "@tscircuit/math-utils"
|
|
2
|
-
|
|
3
|
-
// Point: { x: number, y: number }
|
|
4
|
-
// Bounds: { minX: number, minY: number, maxX: number, maxY: number }
|
|
5
|
-
|
|
6
|
-
export interface WaypointPair {
|
|
7
|
-
start: Point
|
|
8
|
-
end: Point
|
|
9
|
-
|
|
10
|
-
// Waypoints may be part of the same network- in these cases they are allowed
|
|
11
|
-
// to intersect with each other
|
|
12
|
-
networkId?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface Obstacle extends Bounds {
|
|
16
|
-
center: Point
|
|
17
|
-
|
|
18
|
-
networkId?: string
|
|
19
|
-
|
|
20
|
-
// Computed prior to the solver running
|
|
21
|
-
outerSegments?: [Point, Point][]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface CurvyTraceProblem {
|
|
25
|
-
bounds: Bounds
|
|
26
|
-
waypointPairs: WaypointPair[]
|
|
27
|
-
obstacles: Obstacle[]
|
|
28
|
-
preferredSpacing: number
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface OutputTrace {
|
|
32
|
-
waypointPair: WaypointPair
|
|
33
|
-
points: Point[]
|
|
34
|
-
networkId?: string
|
|
35
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export const hashString = (str: string) => {
|
|
2
|
-
let hash = 0
|
|
3
|
-
for (let i = 0; i < str.length; i++) {
|
|
4
|
-
hash = str.charCodeAt(i) * 779 + ((hash << 5) - hash)
|
|
5
|
-
}
|
|
6
|
-
return hash
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const getColorForNetworkId = (networkId?: string | null) => {
|
|
10
|
-
if (!networkId) return "rgba(0, 0, 0, 0.5)"
|
|
11
|
-
return `hsl(${hashString(networkId) % 360}, 100%, 50%)`
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export { visualizeCurvyTraceProblem } from "./visualizeCurvyTraceProblem"
|