@tscircuit/curvy-trace-solver 0.0.1

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/.claude/settings.local.json +16 -0
  2. package/README.md +3 -0
  3. package/biome.json +96 -0
  4. package/bunfig.toml +5 -0
  5. package/cosmos.config.json +5 -0
  6. package/cosmos.decorator.tsx +21 -0
  7. package/dist/index.d.ts +56 -0
  8. package/dist/index.js +1643 -0
  9. package/fixtures/basics/basics01-input.json +15 -0
  10. package/fixtures/basics/basics01.fixture.tsx +11 -0
  11. package/fixtures/problem-generator.fixture.tsx +50 -0
  12. package/index.html +12 -0
  13. package/lib/CurvyTraceSolver.ts +775 -0
  14. package/lib/geometry/getObstacleOuterSegments.ts +25 -0
  15. package/lib/geometry/index.ts +8 -0
  16. package/lib/index.ts +2 -0
  17. package/lib/problem-generator/countChordCrossings.ts +119 -0
  18. package/lib/problem-generator/createRng.ts +13 -0
  19. package/lib/problem-generator/generateRandomProblem.ts +225 -0
  20. package/lib/problem-generator/index.ts +1 -0
  21. package/lib/problem-generator/randomBoundaryPoint.ts +30 -0
  22. package/lib/problem-generator/wouldCrossAny.ts +16 -0
  23. package/lib/sampleTraceIntoSegments.ts +62 -0
  24. package/lib/scoreOutputCost.ts +116 -0
  25. package/lib/types.ts +35 -0
  26. package/lib/visualization-utils/index.ts +14 -0
  27. package/lib/visualization-utils/visualizeCurvyTraceProblem.ts +66 -0
  28. package/package.json +27 -0
  29. package/scripts/benchmarks/run-benchmark.ts +85 -0
  30. package/tests/__snapshots__/svg.snap.svg +3 -0
  31. package/tests/basics/__snapshots__/basics01.snap.svg +44 -0
  32. package/tests/basics/basics01.test.ts +12 -0
  33. package/tests/fixtures/preload.ts +1 -0
  34. package/tests/svg.test.ts +12 -0
  35. package/tsconfig.json +35 -0
@@ -0,0 +1,25 @@
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
+ }
@@ -0,0 +1,8 @@
1
+ import type { Bounds } from "@tscircuit/math-utils"
2
+
3
+ export const getBoundsCenter = (bounds: Bounds) => {
4
+ return {
5
+ x: (bounds.minX + bounds.maxX) / 2,
6
+ y: (bounds.minY + bounds.maxY) / 2,
7
+ }
8
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./CurvyTraceSolver"
2
+ export * from "./types"
@@ -0,0 +1,119 @@
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
+ }
@@ -0,0 +1,13 @@
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
+ }
@@ -0,0 +1,225 @@
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
+ }
@@ -0,0 +1 @@
1
+ export * from "./generateRandomProblem"
@@ -0,0 +1,30 @@
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
+ }
@@ -0,0 +1,16 @@
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
+ }
@@ -0,0 +1,62 @@
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
+ }
@@ -0,0 +1,116 @@
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 ADDED
@@ -0,0 +1,35 @@
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
+ }
@@ -0,0 +1,14 @@
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"