@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
package/lib/CurvyTraceSolver.ts
DELETED
|
@@ -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
|
-
}
|