@tscircuit/curvy-trace-solver 0.0.2 → 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.
@@ -1,775 +0,0 @@
1
- import { BaseSolver } from "@tscircuit/solver-utils"
2
- import type { CurvyTraceProblem, OutputTrace, WaypointPair } from "./types"
3
- import type { GraphicsObject } from "graphics-debug"
4
- import type { Point, Bounds } from "@tscircuit/math-utils"
5
- import { visualizeCurvyTraceProblem } from "./visualization-utils"
6
- import { getObstacleOuterSegments } from "./geometry/getObstacleOuterSegments"
7
-
8
- interface TraceWithControlPoints {
9
- waypointPair: WaypointPair
10
- ctrl1: Point
11
- ctrl2: Point
12
- networkId?: string
13
- t1: number
14
- t2: number
15
- }
16
-
17
- // Get perimeter position for a point on the boundary
18
- function getPerimeterT(p: Point, bounds: Bounds): number {
19
- const { minX, maxX, minY, maxY } = bounds
20
- const W = maxX - minX
21
- const H = maxY - minY
22
- const eps = 1e-6
23
-
24
- if (Math.abs(p.y - maxY) < eps) return p.x - minX
25
- if (Math.abs(p.x - maxX) < eps) return W + (maxY - p.y)
26
- if (Math.abs(p.y - minY) < eps) return W + H + (maxX - p.x)
27
- if (Math.abs(p.x - minX) < eps) return 2 * W + H + (p.y - minY)
28
- return 0
29
- }
30
-
31
- // Get point on perimeter at position t
32
- function getPerimeterPoint(t: number, bounds: Bounds): Point {
33
- const { minX, maxX, minY, maxY } = bounds
34
- const W = maxX - minX
35
- const H = maxY - minY
36
- const perimeter = 2 * W + 2 * H
37
- t = ((t % perimeter) + perimeter) % perimeter
38
-
39
- if (t <= W) return { x: minX + t, y: maxY }
40
- if (t <= W + H) return { x: maxX, y: maxY - (t - W) }
41
- if (t <= 2 * W + H) return { x: maxX - (t - W - H), y: minY }
42
- return { x: minX, y: minY + (t - 2 * W - H) }
43
- }
44
-
45
- // Inline bezier sampling for performance - returns points directly into provided array
46
- function sampleCubicBezierInline(
47
- p0x: number,
48
- p0y: number,
49
- p1x: number,
50
- p1y: number,
51
- p2x: number,
52
- p2y: number,
53
- p3x: number,
54
- p3y: number,
55
- points: Float64Array,
56
- numSamples: number,
57
- ): void {
58
- for (let i = 0; i <= numSamples; i++) {
59
- const t = i / numSamples
60
- const mt = 1 - t
61
- const mt2 = mt * mt
62
- const mt3 = mt2 * mt
63
- const t2 = t * t
64
- const t3 = t2 * t
65
- const idx = i * 2
66
- points[idx] = mt3 * p0x + 3 * mt2 * t * p1x + 3 * mt * t2 * p2x + t3 * p3x
67
- points[idx + 1] =
68
- mt3 * p0y + 3 * mt2 * t * p1y + 3 * mt * t2 * p2y + t3 * p3y
69
- }
70
- }
71
-
72
- // Sample a cubic bezier curve into Point array
73
- function sampleCubicBezier(
74
- p0: Point,
75
- p1: Point,
76
- p2: Point,
77
- p3: Point,
78
- numSamples: number,
79
- ): Point[] {
80
- const points: Point[] = []
81
- for (let i = 0; i <= numSamples; i++) {
82
- const t = i / numSamples
83
- const mt = 1 - t
84
- const mt2 = mt * mt
85
- const mt3 = mt2 * mt
86
- const t2 = t * t
87
- const t3 = t2 * t
88
- points.push({
89
- x: mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x,
90
- y: mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y,
91
- })
92
- }
93
- return points
94
- }
95
-
96
- // Point to segment distance squared
97
- function ptSegDistSq(
98
- px: number,
99
- py: number,
100
- sx: number,
101
- sy: number,
102
- ex: number,
103
- ey: number,
104
- ): number {
105
- const dx = ex - sx
106
- const dy = ey - sy
107
- const lenSq = dx * dx + dy * dy
108
- if (lenSq === 0) {
109
- const dpx = px - sx
110
- const dpy = py - sy
111
- return dpx * dpx + dpy * dpy
112
- }
113
- const t = Math.max(0, Math.min(1, ((px - sx) * dx + (py - sy) * dy) / lenSq))
114
- const projX = sx + t * dx
115
- const projY = sy + t * dy
116
- const dpx = px - projX
117
- const dpy = py - projY
118
- return dpx * dpx + dpy * dpy
119
- }
120
-
121
- // Inlined segment-to-segment distance squared for hot path
122
- function segmentDistSq(
123
- a1x: number,
124
- a1y: number,
125
- a2x: number,
126
- a2y: number,
127
- b1x: number,
128
- b1y: number,
129
- b2x: number,
130
- b2y: number,
131
- ): number {
132
- // Check for intersection first
133
- const d1 = (b2x - b1x) * (a1y - b1y) - (b2y - b1y) * (a1x - b1x)
134
- const d2 = (b2x - b1x) * (a2y - b1y) - (b2y - b1y) * (a2x - b1x)
135
- const d3 = (a2x - a1x) * (b1y - a1y) - (a2y - a1y) * (b1x - a1x)
136
- const d4 = (a2x - a1x) * (b2y - a1y) - (a2y - a1y) * (b2x - a1x)
137
-
138
- if (
139
- ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) &&
140
- ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))
141
- ) {
142
- return 0
143
- }
144
-
145
- return Math.min(
146
- ptSegDistSq(a1x, a1y, b1x, b1y, b2x, b2y),
147
- ptSegDistSq(a2x, a2y, b1x, b1y, b2x, b2y),
148
- ptSegDistSq(b1x, b1y, a1x, a1y, a2x, a2y),
149
- ptSegDistSq(b2x, b2y, a1x, a1y, a2x, a2y),
150
- )
151
- }
152
-
153
- // Check if chord A contains chord B
154
- function chordContains(
155
- aT1: number,
156
- aT2: number,
157
- bT1: number,
158
- bT2: number,
159
- perimeter: number,
160
- ): boolean {
161
- const normalize = (t: number) => ((t % perimeter) + perimeter) % perimeter
162
- const a1 = normalize(aT1),
163
- a2 = normalize(aT2)
164
- const b1 = normalize(bT1),
165
- b2 = normalize(bT2)
166
- const [aMin, aMax] = a1 < a2 ? [a1, a2] : [a2, a1]
167
- return b1 > aMin && b1 < aMax && b2 > aMin && b2 < aMax
168
- }
169
-
170
- function getBoundsCenter(bounds: Bounds): Point {
171
- return {
172
- x: (bounds.minX + bounds.maxX) / 2,
173
- y: (bounds.minY + bounds.maxY) / 2,
174
- }
175
- }
176
-
177
- // Compute bounding box for a sampled trace
178
- function computeTraceBounds(
179
- points: Float64Array,
180
- numPoints: number,
181
- ): { minX: number; maxX: number; minY: number; maxY: number } {
182
- let minX = Infinity,
183
- maxX = -Infinity,
184
- minY = Infinity,
185
- maxY = -Infinity
186
- for (let i = 0; i < numPoints; i++) {
187
- const x = points[i * 2],
188
- y = points[i * 2 + 1]
189
- if (x < minX) minX = x
190
- if (x > maxX) maxX = x
191
- if (y < minY) minY = y
192
- if (y > maxY) maxY = y
193
- }
194
- return { minX, maxX, minY, maxY }
195
- }
196
-
197
- const OPT_SAMPLES = 5 // Samples during optimization
198
- const OUTPUT_SAMPLES = 20 // Samples for final output
199
-
200
- export class CurvyTraceSolver extends BaseSolver {
201
- outputTraces: OutputTrace[] = []
202
- private traces: TraceWithControlPoints[] = []
203
- private optimizationStep = 0
204
- private readonly maxOptimizationSteps = 100
205
-
206
- // Typed arrays for faster sampling
207
- private sampledPoints: Float64Array[] = []
208
- private traceBounds: {
209
- minX: number
210
- maxX: number
211
- minY: number
212
- maxY: number
213
- }[] = []
214
-
215
- // Pre-computed obstacle segments as flat arrays
216
- private obstacleSegments: Float64Array = new Float64Array(0)
217
- private obstacleNetworkIds: (string | undefined)[] = []
218
- private numObstacleSegments = 0
219
-
220
- // Collision pairs cache - which traces can possibly collide
221
- private collisionPairs: [number, number][] = []
222
-
223
- private lastCost = Infinity
224
- private stagnantSteps = 0
225
-
226
- constructor(public problem: CurvyTraceProblem) {
227
- super()
228
- for (const obstacle of this.problem.obstacles) {
229
- obstacle.outerSegments = getObstacleOuterSegments(obstacle)
230
- }
231
- this.precomputeObstacles()
232
- }
233
-
234
- override getConstructorParams() {
235
- return this.problem
236
- }
237
-
238
- private precomputeObstacles() {
239
- const { obstacles } = this.problem
240
- let totalSegments = 0
241
- for (const obs of obstacles) {
242
- if (obs.outerSegments) totalSegments += obs.outerSegments.length
243
- }
244
-
245
- this.obstacleSegments = new Float64Array(totalSegments * 4)
246
- this.obstacleNetworkIds = []
247
- this.numObstacleSegments = totalSegments
248
-
249
- let idx = 0
250
- for (const obs of obstacles) {
251
- if (obs.outerSegments) {
252
- for (const seg of obs.outerSegments) {
253
- this.obstacleSegments[idx++] = seg[0].x
254
- this.obstacleSegments[idx++] = seg[0].y
255
- this.obstacleSegments[idx++] = seg[1].x
256
- this.obstacleSegments[idx++] = seg[1].y
257
- this.obstacleNetworkIds.push(obs.networkId)
258
- }
259
- }
260
- }
261
- }
262
-
263
- private initializeTraces() {
264
- const { bounds, waypointPairs } = this.problem
265
- const { minX, maxX, minY, maxY } = bounds
266
- const W = maxX - minX
267
- const H = maxY - minY
268
- const perimeter = 2 * W + 2 * H
269
- const center = getBoundsCenter(bounds)
270
-
271
- const tracesWithT = waypointPairs.map((pair, idx) => ({
272
- pair,
273
- t1: getPerimeterT(pair.start, bounds),
274
- t2: getPerimeterT(pair.end, bounds),
275
- idx,
276
- }))
277
-
278
- const nestingDepth = new Map<number, number>()
279
- for (const trace of tracesWithT) {
280
- let depth = 0
281
- for (const other of tracesWithT) {
282
- if (
283
- trace.idx !== other.idx &&
284
- chordContains(other.t1, other.t2, trace.t1, trace.t2, perimeter)
285
- ) {
286
- depth++
287
- }
288
- }
289
- nestingDepth.set(trace.idx, depth)
290
- }
291
-
292
- const maxDepth = Math.max(...Array.from(nestingDepth.values()), 1)
293
-
294
- this.traces = tracesWithT.map(({ pair, t1, t2, idx }) => {
295
- let dt = t2 - t1
296
- if (dt > perimeter / 2) dt -= perimeter
297
- if (dt < -perimeter / 2) dt += perimeter
298
-
299
- const tCtrl1 = t1 + dt * 0.33
300
- const tCtrl2 = t1 + dt * 0.67
301
-
302
- const pPerim1 = getPerimeterPoint(tCtrl1, bounds)
303
- const pPerim2 = getPerimeterPoint(tCtrl2, bounds)
304
-
305
- const pLinear1 = {
306
- x: pair.start.x + (pair.end.x - pair.start.x) * 0.33,
307
- y: pair.start.y + (pair.end.y - pair.start.y) * 0.33,
308
- }
309
- const pLinear2 = {
310
- x: pair.start.x + (pair.end.x - pair.start.x) * 0.67,
311
- y: pair.start.y + (pair.end.y - pair.start.y) * 0.67,
312
- }
313
-
314
- const midPoint = {
315
- x: (pair.start.x + pair.end.x) / 2,
316
- y: (pair.start.y + pair.end.y) / 2,
317
- }
318
- const distToCenter = Math.hypot(
319
- midPoint.x - center.x,
320
- midPoint.y - center.y,
321
- )
322
- const maxDist = Math.hypot(W / 2, H / 2)
323
- const spatialDepth = 1 - distToCenter / maxDist
324
-
325
- const depth = nestingDepth.get(idx) || 0
326
- const normalizedDepth = depth / maxDepth
327
-
328
- const basePull = 0.3 + spatialDepth * 0.4
329
- const pullAmount = Math.max(0.05, basePull - normalizedDepth * 0.2)
330
-
331
- return {
332
- waypointPair: pair,
333
- ctrl1: {
334
- x: pLinear1.x * (1 - pullAmount) + pPerim1.x * pullAmount,
335
- y: pLinear1.y * (1 - pullAmount) + pPerim1.y * pullAmount,
336
- },
337
- ctrl2: {
338
- x: pLinear2.x * (1 - pullAmount) + pPerim2.x * pullAmount,
339
- y: pLinear2.y * (1 - pullAmount) + pPerim2.y * pullAmount,
340
- },
341
- networkId: pair.networkId,
342
- t1,
343
- t2,
344
- }
345
- })
346
-
347
- // Initialize typed arrays for sampling
348
- this.sampledPoints = this.traces.map(
349
- () => new Float64Array((OPT_SAMPLES + 1) * 2),
350
- )
351
- this.traceBounds = this.traces.map(() => ({
352
- minX: 0,
353
- maxX: 0,
354
- minY: 0,
355
- maxY: 0,
356
- }))
357
-
358
- this.updateSampledTraces()
359
- this.updateCollisionPairs()
360
- }
361
-
362
- private updateSampledTraces() {
363
- for (let i = 0; i < this.traces.length; i++) {
364
- const trace = this.traces[i]
365
- sampleCubicBezierInline(
366
- trace.waypointPair.start.x,
367
- trace.waypointPair.start.y,
368
- trace.ctrl1.x,
369
- trace.ctrl1.y,
370
- trace.ctrl2.x,
371
- trace.ctrl2.y,
372
- trace.waypointPair.end.x,
373
- trace.waypointPair.end.y,
374
- this.sampledPoints[i],
375
- OPT_SAMPLES,
376
- )
377
- this.traceBounds[i] = computeTraceBounds(
378
- this.sampledPoints[i],
379
- OPT_SAMPLES + 1,
380
- )
381
- }
382
- }
383
-
384
- private updateSingleTraceSample(i: number) {
385
- const trace = this.traces[i]
386
- sampleCubicBezierInline(
387
- trace.waypointPair.start.x,
388
- trace.waypointPair.start.y,
389
- trace.ctrl1.x,
390
- trace.ctrl1.y,
391
- trace.ctrl2.x,
392
- trace.ctrl2.y,
393
- trace.waypointPair.end.x,
394
- trace.waypointPair.end.y,
395
- this.sampledPoints[i],
396
- OPT_SAMPLES,
397
- )
398
- this.traceBounds[i] = computeTraceBounds(
399
- this.sampledPoints[i],
400
- OPT_SAMPLES + 1,
401
- )
402
- }
403
-
404
- // Determine which trace pairs could possibly collide based on bounding boxes
405
- private updateCollisionPairs() {
406
- const { preferredSpacing } = this.problem
407
- this.collisionPairs = []
408
-
409
- for (let i = 0; i < this.traces.length; i++) {
410
- for (let j = i + 1; j < this.traces.length; j++) {
411
- const ti = this.traces[i],
412
- tj = this.traces[j]
413
- if (ti.networkId && tj.networkId && ti.networkId === tj.networkId)
414
- continue
415
-
416
- const bi = this.traceBounds[i],
417
- bj = this.traceBounds[j]
418
- // Check if bounding boxes (expanded by preferredSpacing) overlap
419
- if (
420
- bi.maxX + preferredSpacing >= bj.minX &&
421
- bj.maxX + preferredSpacing >= bi.minX &&
422
- bi.maxY + preferredSpacing >= bj.minY &&
423
- bj.maxY + preferredSpacing >= bi.minY
424
- ) {
425
- this.collisionPairs.push([i, j])
426
- }
427
- }
428
- }
429
- }
430
-
431
- private computeTotalCost(): number {
432
- const { preferredSpacing } = this.problem
433
- const spacingSq = preferredSpacing * preferredSpacing
434
- let cost = 0
435
-
436
- // Cost between traces using collision pairs
437
- for (const [i, j] of this.collisionPairs) {
438
- const pi = this.sampledPoints[i],
439
- pj = this.sampledPoints[j]
440
-
441
- for (let a = 0; a < OPT_SAMPLES; a++) {
442
- const a1x = pi[a * 2],
443
- a1y = pi[a * 2 + 1]
444
- const a2x = pi[(a + 1) * 2],
445
- a2y = pi[(a + 1) * 2 + 1]
446
-
447
- for (let b = 0; b < OPT_SAMPLES; b++) {
448
- const b1x = pj[b * 2],
449
- b1y = pj[b * 2 + 1]
450
- const b2x = pj[(b + 1) * 2],
451
- b2y = pj[(b + 1) * 2 + 1]
452
-
453
- const distSq = segmentDistSq(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y)
454
- if (distSq < spacingSq) {
455
- const dist = Math.sqrt(distSq)
456
- cost += (preferredSpacing - dist) ** 2
457
- if (distSq < 1e-18) cost += 20 * spacingSq
458
- }
459
- }
460
- }
461
- }
462
-
463
- // Cost against obstacles
464
- for (let i = 0; i < this.traces.length; i++) {
465
- const trace = this.traces[i]
466
- const pi = this.sampledPoints[i]
467
- const bi = this.traceBounds[i]
468
-
469
- for (let obsIdx = 0; obsIdx < this.numObstacleSegments; obsIdx++) {
470
- if (
471
- trace.networkId &&
472
- this.obstacleNetworkIds[obsIdx] &&
473
- trace.networkId === this.obstacleNetworkIds[obsIdx]
474
- )
475
- continue
476
-
477
- const obsBase = obsIdx * 4
478
- const ox1 = this.obstacleSegments[obsBase]
479
- const oy1 = this.obstacleSegments[obsBase + 1]
480
- const ox2 = this.obstacleSegments[obsBase + 2]
481
- const oy2 = this.obstacleSegments[obsBase + 3]
482
-
483
- // Quick bounds check
484
- const obsMinX = Math.min(ox1, ox2),
485
- obsMaxX = Math.max(ox1, ox2)
486
- const obsMinY = Math.min(oy1, oy2),
487
- obsMaxY = Math.max(oy1, oy2)
488
- if (
489
- bi.maxX + preferredSpacing < obsMinX ||
490
- obsMaxX + preferredSpacing < bi.minX ||
491
- bi.maxY + preferredSpacing < obsMinY ||
492
- obsMaxY + preferredSpacing < bi.minY
493
- )
494
- continue
495
-
496
- for (let a = 0; a < OPT_SAMPLES; a++) {
497
- const a1x = pi[a * 2],
498
- a1y = pi[a * 2 + 1]
499
- const a2x = pi[(a + 1) * 2],
500
- a2y = pi[(a + 1) * 2 + 1]
501
-
502
- const distSq = segmentDistSq(a1x, a1y, a2x, a2y, ox1, oy1, ox2, oy2)
503
- if (distSq < spacingSq) {
504
- const dist = Math.sqrt(distSq)
505
- cost += (preferredSpacing - dist) ** 2
506
- if (distSq < 1e-18) cost += 20 * spacingSq
507
- }
508
- }
509
- }
510
- }
511
-
512
- return cost
513
- }
514
-
515
- private computeCostForTrace(traceIdx: number): number {
516
- const { preferredSpacing } = this.problem
517
- const spacingSq = preferredSpacing * preferredSpacing
518
- const trace = this.traces[traceIdx]
519
- const pi = this.sampledPoints[traceIdx]
520
- const bi = this.traceBounds[traceIdx]
521
- let cost = 0
522
-
523
- // Cost against other traces
524
- for (let j = 0; j < this.traces.length; j++) {
525
- if (j === traceIdx) continue
526
- const other = this.traces[j]
527
- if (
528
- trace.networkId &&
529
- other.networkId &&
530
- trace.networkId === other.networkId
531
- )
532
- continue
533
-
534
- const bj = this.traceBounds[j]
535
- // Bounding box check
536
- if (
537
- bi.maxX + preferredSpacing < bj.minX ||
538
- bj.maxX + preferredSpacing < bi.minX ||
539
- bi.maxY + preferredSpacing < bj.minY ||
540
- bj.maxY + preferredSpacing < bi.minY
541
- )
542
- continue
543
-
544
- const pj = this.sampledPoints[j]
545
- for (let a = 0; a < OPT_SAMPLES; a++) {
546
- const a1x = pi[a * 2],
547
- a1y = pi[a * 2 + 1]
548
- const a2x = pi[(a + 1) * 2],
549
- a2y = pi[(a + 1) * 2 + 1]
550
-
551
- for (let b = 0; b < OPT_SAMPLES; b++) {
552
- const b1x = pj[b * 2],
553
- b1y = pj[b * 2 + 1]
554
- const b2x = pj[(b + 1) * 2],
555
- b2y = pj[(b + 1) * 2 + 1]
556
-
557
- const distSq = segmentDistSq(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y)
558
- if (distSq < spacingSq) {
559
- const dist = Math.sqrt(distSq)
560
- cost += (preferredSpacing - dist) ** 2
561
- if (distSq < 1e-18) cost += 20 * spacingSq
562
- }
563
- }
564
- }
565
- }
566
-
567
- // Cost against obstacles
568
- for (let obsIdx = 0; obsIdx < this.numObstacleSegments; obsIdx++) {
569
- if (
570
- trace.networkId &&
571
- this.obstacleNetworkIds[obsIdx] &&
572
- trace.networkId === this.obstacleNetworkIds[obsIdx]
573
- )
574
- continue
575
-
576
- const obsBase = obsIdx * 4
577
- const ox1 = this.obstacleSegments[obsBase]
578
- const oy1 = this.obstacleSegments[obsBase + 1]
579
- const ox2 = this.obstacleSegments[obsBase + 2]
580
- const oy2 = this.obstacleSegments[obsBase + 3]
581
-
582
- const obsMinX = Math.min(ox1, ox2),
583
- obsMaxX = Math.max(ox1, ox2)
584
- const obsMinY = Math.min(oy1, oy2),
585
- obsMaxY = Math.max(oy1, oy2)
586
- if (
587
- bi.maxX + preferredSpacing < obsMinX ||
588
- obsMaxX + preferredSpacing < bi.minX ||
589
- bi.maxY + preferredSpacing < obsMinY ||
590
- obsMaxY + preferredSpacing < bi.minY
591
- )
592
- continue
593
-
594
- for (let a = 0; a < OPT_SAMPLES; a++) {
595
- const a1x = pi[a * 2],
596
- a1y = pi[a * 2 + 1]
597
- const a2x = pi[(a + 1) * 2],
598
- a2y = pi[(a + 1) * 2 + 1]
599
-
600
- const distSq = segmentDistSq(a1x, a1y, a2x, a2y, ox1, oy1, ox2, oy2)
601
- if (distSq < spacingSq) {
602
- const dist = Math.sqrt(distSq)
603
- cost += (preferredSpacing - dist) ** 2
604
- if (distSq < 1e-18) cost += 20 * spacingSq
605
- }
606
- }
607
- }
608
-
609
- return cost
610
- }
611
-
612
- private optimizeStep() {
613
- const { bounds } = this.problem
614
- const { minX, maxX, minY, maxY } = bounds
615
-
616
- // Adaptive step size
617
- const progress = this.optimizationStep / this.maxOptimizationSteps
618
- const baseStep = 3.5 * (1 - progress) + 0.5
619
- const SQRT1_2 = Math.SQRT1_2
620
-
621
- // Sort traces by cost (worst first)
622
- const traceCosts: { idx: number; cost: number }[] = []
623
- for (let i = 0; i < this.traces.length; i++) {
624
- traceCosts.push({ idx: i, cost: this.computeCostForTrace(i) })
625
- }
626
- traceCosts.sort((a, b) => b.cost - a.cost)
627
-
628
- for (const { idx: i, cost: currentCost } of traceCosts) {
629
- if (currentCost === 0) continue
630
-
631
- const trace = this.traces[i]
632
- const steps = [baseStep, baseStep * 1.5, baseStep * 0.5]
633
-
634
- for (const step of steps) {
635
- const directions = [
636
- { dx: step, dy: 0 },
637
- { dx: -step, dy: 0 },
638
- { dx: 0, dy: step },
639
- { dx: 0, dy: -step },
640
- { dx: step * SQRT1_2, dy: step * SQRT1_2 },
641
- { dx: -step * SQRT1_2, dy: -step * SQRT1_2 },
642
- { dx: step * SQRT1_2, dy: -step * SQRT1_2 },
643
- { dx: -step * SQRT1_2, dy: step * SQRT1_2 },
644
- ]
645
-
646
- let bestCost = this.computeCostForTrace(i)
647
- let bestCtrl1x = trace.ctrl1.x
648
- let bestCtrl1y = trace.ctrl1.y
649
- let bestCtrl2x = trace.ctrl2.x
650
- let bestCtrl2y = trace.ctrl2.y
651
-
652
- const origCtrl1x = trace.ctrl1.x
653
- const origCtrl1y = trace.ctrl1.y
654
- const origCtrl2x = trace.ctrl2.x
655
- const origCtrl2y = trace.ctrl2.y
656
-
657
- for (const dir of directions) {
658
- // Try ctrl1
659
- trace.ctrl1.x = Math.max(minX, Math.min(maxX, origCtrl1x + dir.dx))
660
- trace.ctrl1.y = Math.max(minY, Math.min(maxY, origCtrl1y + dir.dy))
661
- this.updateSingleTraceSample(i)
662
- const cost1 = this.computeCostForTrace(i)
663
- if (cost1 < bestCost) {
664
- bestCost = cost1
665
- bestCtrl1x = trace.ctrl1.x
666
- bestCtrl1y = trace.ctrl1.y
667
- bestCtrl2x = origCtrl2x
668
- bestCtrl2y = origCtrl2y
669
- }
670
- trace.ctrl1.x = origCtrl1x
671
- trace.ctrl1.y = origCtrl1y
672
-
673
- // Try ctrl2
674
- trace.ctrl2.x = Math.max(minX, Math.min(maxX, origCtrl2x + dir.dx))
675
- trace.ctrl2.y = Math.max(minY, Math.min(maxY, origCtrl2y + dir.dy))
676
- this.updateSingleTraceSample(i)
677
- const cost2 = this.computeCostForTrace(i)
678
- if (cost2 < bestCost) {
679
- bestCost = cost2
680
- bestCtrl1x = origCtrl1x
681
- bestCtrl1y = origCtrl1y
682
- bestCtrl2x = trace.ctrl2.x
683
- bestCtrl2y = trace.ctrl2.y
684
- }
685
- trace.ctrl2.x = origCtrl2x
686
- trace.ctrl2.y = origCtrl2y
687
-
688
- // Try both together
689
- trace.ctrl1.x = Math.max(minX, Math.min(maxX, origCtrl1x + dir.dx))
690
- trace.ctrl1.y = Math.max(minY, Math.min(maxY, origCtrl1y + dir.dy))
691
- trace.ctrl2.x = Math.max(minX, Math.min(maxX, origCtrl2x + dir.dx))
692
- trace.ctrl2.y = Math.max(minY, Math.min(maxY, origCtrl2y + dir.dy))
693
- this.updateSingleTraceSample(i)
694
- const cost3 = this.computeCostForTrace(i)
695
- if (cost3 < bestCost) {
696
- bestCost = cost3
697
- bestCtrl1x = trace.ctrl1.x
698
- bestCtrl1y = trace.ctrl1.y
699
- bestCtrl2x = trace.ctrl2.x
700
- bestCtrl2y = trace.ctrl2.y
701
- }
702
- trace.ctrl1.x = origCtrl1x
703
- trace.ctrl1.y = origCtrl1y
704
- trace.ctrl2.x = origCtrl2x
705
- trace.ctrl2.y = origCtrl2y
706
- }
707
-
708
- trace.ctrl1.x = bestCtrl1x
709
- trace.ctrl1.y = bestCtrl1y
710
- trace.ctrl2.x = bestCtrl2x
711
- trace.ctrl2.y = bestCtrl2y
712
- this.updateSingleTraceSample(i)
713
-
714
- if (bestCost < currentCost * 0.9) break // Found significant improvement
715
- }
716
- }
717
-
718
- // Update collision pairs periodically
719
- if (this.optimizationStep % 10 === 0) {
720
- this.updateCollisionPairs()
721
- }
722
- }
723
-
724
- private buildOutputTraces() {
725
- this.outputTraces = this.traces.map((trace) => ({
726
- waypointPair: trace.waypointPair,
727
- points: sampleCubicBezier(
728
- trace.waypointPair.start,
729
- trace.ctrl1,
730
- trace.ctrl2,
731
- trace.waypointPair.end,
732
- OUTPUT_SAMPLES,
733
- ),
734
- networkId: trace.networkId,
735
- }))
736
- }
737
-
738
- override _step() {
739
- if (this.traces.length === 0) {
740
- this.initializeTraces()
741
- this.lastCost = this.computeTotalCost()
742
- this.stagnantSteps = 0
743
- }
744
-
745
- if (this.optimizationStep < this.maxOptimizationSteps) {
746
- this.optimizeStep()
747
- this.optimizationStep++
748
-
749
- const currentCost = this.computeTotalCost()
750
- if (currentCost === 0) {
751
- this.optimizationStep = this.maxOptimizationSteps
752
- } else if (currentCost >= this.lastCost * 0.99) {
753
- this.stagnantSteps++
754
- if (this.stagnantSteps > 15) {
755
- this.optimizationStep = this.maxOptimizationSteps
756
- }
757
- } else {
758
- this.stagnantSteps = 0
759
- }
760
- this.lastCost = currentCost
761
- }
762
-
763
- if (this.optimizationStep >= this.maxOptimizationSteps) {
764
- this.buildOutputTraces()
765
- this.solved = true
766
- }
767
- }
768
-
769
- override visualize(): GraphicsObject {
770
- if (this.traces.length > 0) {
771
- this.buildOutputTraces()
772
- }
773
- return visualizeCurvyTraceProblem(this.problem, this.outputTraces)
774
- }
775
- }