A circuit from ten waypoints
The demo needed a real circuit — mostly straights you can open up on, joined
by corners that demand braking. Not a loop; a layout. It comes from ten
waypoints in src/track.ts, each carrying a fillet radius.
The construction
control polygon → per-vertex fillets → CurvePath → uniform arc-length samples
Each vertex becomes a quadratic bezier fillet with tangent distance
t = r / tan(θ/2) — the standard rounded-corner relation — capped at 45% of
the shorter adjacent segment so corners can't swallow their straights. The
straights between exits are plain LineCurve3s. THREE.CurvePath samples
the closed loop evenly by arc length, which is what makes every downstream
consumer trivial: gates, the asphalt ribbon, and the autopilot all index the
same centerline array.
The bug the sanity check caught
First version reported a minimum corner radius of 1.6 m — a spike, not a
designed corner. It sat at the loop's closing vertex, which had r: 0
because "the straight begins there." A closed path has no free ends: the
closing vertex is a corner too, and an unfilleted vertex is a point — an
infinite-curvature corner, exactly where the crash rule would fire for no
visible reason. Radius 14 fixed it.
Lesson worth keeping: numeric invariants beat eyeballs. A ten-line script printing the radius histogram found it in seconds; it would have looked fine in render and only shown up as an inexplicable crash mid-lap.
Curvature is sampled, not derived
Per-sample radius comes from finite differences of tangents
(κ = dθ/ds), then one light moving-average pass — enough to kill noise,
not enough to bleed corner curvature into the straights. First version
smoothed three passes and the "straight" fraction collapsed from ~60% to 35%:
the corners were leaking into the roads. Measurable, so fixable.
The final distribution: ~57% of the lap at r > 40 m (flat out), a hairpin at r ≈ 10 m that demands real braking, sweepers in between. The shape was designed by editing numbers, not by pushing spline handles.