@inventure71/paddockjs 1.0.0 → 2.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 (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +4 -0
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,659 @@
1
+ import { clamp, wrapDistance } from '../simMath.js';
2
+ import { metersToSimUnits } from '../units.js';
3
+ import {
4
+ candidateIdsFromGrid,
5
+ candidateIdsFromGridBounds,
6
+ createSpatialGrid,
7
+ gridCellForPoint,
8
+ insertIdIntoGridBounds,
9
+ } from './trackQueryGrid.js';
10
+ import { createPitQueryIndex } from './trackQueryPitIndex.js';
11
+ import { createQueryScratch, ensureQueryScratch, ensureScratchArray, nextScratchEpoch } from './trackQueryScratch.js';
12
+ import { createQueryStats, recordFallback, recordStat } from './trackQueryStats.js';
13
+
14
+ export {
15
+ queryPitBoxCandidates,
16
+ queryPitRoadSegmentCandidates,
17
+ queryPitRoadSegmentCandidatesByRoute,
18
+ } from './trackQueryPitIndex.js';
19
+
20
+ const DEFAULT_GRID_CELL_SIZE = metersToSimUnits(32);
21
+ const GRID_NEIGHBOR_LIMIT = 2;
22
+ const ARC_BUCKET_COUNT = 512;
23
+ const AMBIGUOUS_DISTANCE_EPSILON = 1e-6;
24
+ const RAY_TRACE_BUCKET_DEGREES = 10;
25
+
26
+ export function createTrackQueryIndex(track) {
27
+ const samples = Array.isArray(track.samples) ? track.samples : [];
28
+ const segmentCount = Math.max(0, samples.length - 1);
29
+ const bands = createTrackBands(track);
30
+ const expansion = bands.runoffEdge + (track.barrierWidth ?? 0) + metersToSimUnits(64);
31
+ const bounds = expandedSampleBounds(samples, expansion);
32
+ const centerline = createCenterlineSegments(samples, segmentCount);
33
+ const grid = createSpatialGrid(bounds, DEFAULT_GRID_CELL_SIZE);
34
+ const segmentGrid = createSpatialGrid(bounds, DEFAULT_GRID_CELL_SIZE);
35
+ const arcBuckets = createArcBuckets(track.length, ARC_BUCKET_COUNT);
36
+
37
+ for (let segmentId = 0; segmentId < segmentCount; segmentId += 1) {
38
+ insertSegmentIntoGrid(grid, centerline, segmentId, expansion);
39
+ insertSegmentIntoGrid(segmentGrid, centerline, segmentId, 0);
40
+ insertSegmentIntoArcBuckets(arcBuckets, centerline, segmentId);
41
+ }
42
+
43
+ const index = {
44
+ version: 1,
45
+ bands,
46
+ centerline,
47
+ grid,
48
+ segmentGrid,
49
+ arcBuckets,
50
+ pit: createPitQueryIndex(track, grid.cellSize),
51
+ stats: createQueryStats(),
52
+ };
53
+ index.queryScratch = createQueryScratch(index);
54
+ return index;
55
+ }
56
+
57
+ export function queryNearestTrackProjection(track, position, progressHint = null, options = {}) {
58
+ const index = track?.queryIndex;
59
+ if (!index?.centerline?.segmentCount) {
60
+ recordFallback(index, 'nearestFallbackReasons', 'missing-index');
61
+ return null;
62
+ }
63
+ if (!finitePoint(position)) {
64
+ recordFallback(index, 'nearestFallbackReasons', 'invalid-position');
65
+ return null;
66
+ }
67
+ index.stats.nearestQueries += 1;
68
+
69
+ const mode = options.indexMode === 'sample' ? 'sample' : 'projection';
70
+ const { projection, path, reason } = bestNearestFromIndex(track, index, position, progressHint, mode, options);
71
+ if (!projection) {
72
+ recordFallback(index, 'nearestFallbackReasons', reason ?? 'unknown');
73
+ return null;
74
+ }
75
+
76
+ recordStat(index, 'nearestPaths', path);
77
+ return projection;
78
+ }
79
+
80
+ function bestNearestFromIndex(track, index, position, progressHint, mode, options) {
81
+ return mode === 'sample'
82
+ ? bestSampleFromIndex(track, index, position, progressHint, options)
83
+ : bestProjectionFromIndex(index, position, progressHint, options);
84
+ }
85
+
86
+ function bestProjectionFromIndex(index, position, progressHint, options) {
87
+ const hintedCandidateIds = Number.isFinite(progressHint)
88
+ ? candidateIdsFromArcBuckets(index, progressHint, 2)
89
+ : [];
90
+ if (hintedCandidateIds.length) {
91
+ const hinted = bestProjectionFromCandidates(index.centerline, hintedCandidateIds, position, progressHint);
92
+ const maxHintDistance = resolveHintMaxDistance(index, options);
93
+ if (hinted.projection && hinted.projection.distanceSquared <= maxHintDistance * maxHintDistance) {
94
+ return {
95
+ projection: hinted.projection,
96
+ path: hinted.reason === 'tie-resolved' ? 'arc-hint-tie-resolved' : 'arc-hint',
97
+ };
98
+ }
99
+ recordStat(index, 'nearestPaths', hinted.projection ? 'arc-hint-rejected-distance' : `arc-hint-${hinted.reason}`);
100
+ }
101
+
102
+ const gridCandidateIds = candidateIdsFromGrid(index, index.grid, position, GRID_NEIGHBOR_LIMIT);
103
+ const grid = bestProjectionFromCandidates(index.centerline, gridCandidateIds, position, progressHint);
104
+ return grid.projection
105
+ ? {
106
+ projection: grid.projection,
107
+ path: grid.reason === 'tie-resolved' ? 'spatial-grid-tie-resolved' : 'spatial-grid',
108
+ }
109
+ : { projection: null, reason: `spatial-grid-${grid.reason}` };
110
+ }
111
+
112
+ function bestSampleFromIndex(track, index, position, progressHint, options) {
113
+ const samples = track?.samples;
114
+ const sampleCount = Math.max(0, samples?.length - 1);
115
+ if (sampleCount <= 0) return { projection: null, reason: 'missing-samples' };
116
+ const hintedCandidateIds = Number.isFinite(progressHint)
117
+ ? candidateIdsFromArcBuckets(index, progressHint, 2)
118
+ : [];
119
+ if (hintedCandidateIds.length) {
120
+ const hinted = bestSampleFromCandidates(index, samples, sampleCount, hintedCandidateIds, position, progressHint);
121
+ const maxHintDistance = resolveHintMaxDistance(index, options);
122
+ if (hinted.sample && hinted.distanceSquared <= maxHintDistance * maxHintDistance) {
123
+ return {
124
+ projection: hinted.sample,
125
+ path: hinted.reason === 'tie-resolved' ? 'arc-hint-tie-resolved' : 'arc-hint',
126
+ };
127
+ }
128
+ recordStat(index, 'nearestPaths', hinted.sample ? 'arc-hint-rejected-distance' : `arc-hint-${hinted.reason}`);
129
+ }
130
+
131
+ const gridCandidateIds = candidateIdsFromGrid(index, index.grid, position, GRID_NEIGHBOR_LIMIT);
132
+ const grid = bestSampleFromCandidates(index, samples, sampleCount, gridCandidateIds, position, progressHint);
133
+ return grid.sample
134
+ ? {
135
+ projection: grid.sample,
136
+ path: grid.reason === 'tie-resolved' ? 'spatial-grid-tie-resolved' : 'spatial-grid',
137
+ }
138
+ : { projection: null, reason: `spatial-grid-${grid.reason}` };
139
+ }
140
+
141
+ function resolveHintMaxDistance(index, options = {}) {
142
+ if (options.hintMaxDistance == null) return index.bands.runoffEdge + metersToSimUnits(24);
143
+ if (!Number.isFinite(options.hintMaxDistance)) return Infinity;
144
+ return Math.max(0, options.hintMaxDistance);
145
+ }
146
+
147
+ function bestSampleFromCandidates(index, samples, sampleCount, candidateSegmentIds, position, preferredDistance = null) {
148
+ if (!candidateSegmentIds.length || sampleCount <= 0) return { sample: null, reason: 'no-candidates' };
149
+ const scratch = ensureQueryScratch(index);
150
+ const sampleMarks = ensureScratchArray(scratch, 'sampleMarks', sampleCount);
151
+ const sampleEpoch = nextScratchEpoch(scratch, 'sampleEpoch', sampleMarks);
152
+
153
+ let bestSample = null;
154
+ let bestDistanceSquared = Infinity;
155
+ let bestTieScore = Infinity;
156
+ let secondBestDistance = Infinity;
157
+ let tieResolved = false;
158
+ for (const segmentId of candidateSegmentIds) {
159
+ const normalized = ((segmentId % sampleCount) + sampleCount) % sampleCount;
160
+ const adjacent = (normalized + 1) % sampleCount;
161
+ if (sampleMarks[normalized] !== sampleEpoch) {
162
+ sampleMarks[normalized] = sampleEpoch;
163
+ ({ bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved } = considerSampleCandidate({
164
+ samples,
165
+ sampleId: normalized,
166
+ position,
167
+ preferredDistance,
168
+ bestSample,
169
+ bestDistanceSquared,
170
+ bestTieScore,
171
+ secondBestDistance,
172
+ tieResolved,
173
+ }));
174
+ }
175
+ if (sampleMarks[adjacent] !== sampleEpoch) {
176
+ sampleMarks[adjacent] = sampleEpoch;
177
+ ({ bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved } = considerSampleCandidate({
178
+ samples,
179
+ sampleId: adjacent,
180
+ position,
181
+ preferredDistance,
182
+ bestSample,
183
+ bestDistanceSquared,
184
+ bestTieScore,
185
+ secondBestDistance,
186
+ tieResolved,
187
+ }));
188
+ }
189
+ }
190
+
191
+ if (!bestSample) return { sample: null, reason: 'no-best' };
192
+
193
+ const ambiguous = Number.isFinite(secondBestDistance) &&
194
+ Math.abs(secondBestDistance - bestDistanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON;
195
+ return {
196
+ sample: bestSample,
197
+ distanceSquared: bestDistanceSquared,
198
+ reason: ambiguous || tieResolved ? 'tie-resolved' : 'ok',
199
+ };
200
+ }
201
+
202
+ function considerSampleCandidate({
203
+ samples,
204
+ sampleId,
205
+ position,
206
+ preferredDistance,
207
+ bestSample,
208
+ bestDistanceSquared,
209
+ bestTieScore,
210
+ secondBestDistance,
211
+ tieResolved,
212
+ }) {
213
+ const sample = samples[sampleId];
214
+ if (!sample) {
215
+ return { bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved };
216
+ }
217
+ const dx = position.x - sample.x;
218
+ const dy = position.y - sample.y;
219
+ const distanceSquared = dx * dx + dy * dy;
220
+ const tieScore = sampleTieScore(sample, preferredDistance);
221
+ if (!bestSample || distanceSquared < bestDistanceSquared - AMBIGUOUS_DISTANCE_EPSILON) {
222
+ secondBestDistance = bestDistanceSquared;
223
+ bestSample = sample;
224
+ bestDistanceSquared = distanceSquared;
225
+ bestTieScore = tieScore;
226
+ } else if (Math.abs(distanceSquared - bestDistanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON) {
227
+ tieResolved = true;
228
+ secondBestDistance = Math.min(secondBestDistance, distanceSquared);
229
+ if (tieScore < bestTieScore) {
230
+ bestSample = sample;
231
+ bestDistanceSquared = distanceSquared;
232
+ bestTieScore = tieScore;
233
+ }
234
+ } else if (distanceSquared < secondBestDistance) {
235
+ secondBestDistance = distanceSquared;
236
+ }
237
+ return { bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved };
238
+ }
239
+
240
+ function sampleTieScore(sample, preferredDistance) {
241
+ if (!Number.isFinite(preferredDistance)) return sample.distance;
242
+ return Math.abs(sample.distance - preferredDistance);
243
+ }
244
+
245
+ function bestProjectionFromCandidates(centerline, candidateIds, position, preferredDistance = null) {
246
+ if (!candidateIds.length) return { projection: null, reason: 'no-candidates' };
247
+ let best = null;
248
+ let bestTieScore = Infinity;
249
+ let secondBestDistance = Infinity;
250
+ let tieResolved = false;
251
+ for (const segmentId of candidateIds) {
252
+ const projection = projectIndexedSegment(centerline, segmentId, position);
253
+ const tieScore = projectionTieScore(projection, preferredDistance);
254
+ if (!best || projection.distanceSquared < best.distanceSquared - AMBIGUOUS_DISTANCE_EPSILON) {
255
+ secondBestDistance = best?.distanceSquared ?? Infinity;
256
+ best = projection;
257
+ bestTieScore = tieScore;
258
+ } else if (Math.abs(projection.distanceSquared - best.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON) {
259
+ tieResolved = true;
260
+ secondBestDistance = Math.min(secondBestDistance, projection.distanceSquared);
261
+ if (tieScore < bestTieScore) {
262
+ best = projection;
263
+ bestTieScore = tieScore;
264
+ }
265
+ } else if (projection.distanceSquared < secondBestDistance) {
266
+ secondBestDistance = projection.distanceSquared;
267
+ }
268
+ }
269
+
270
+ if (!best) return { projection: null, reason: 'no-best' };
271
+
272
+ const ambiguous = Number.isFinite(secondBestDistance) &&
273
+ Math.abs(secondBestDistance - best.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON;
274
+ return { projection: best, reason: ambiguous || tieResolved ? 'tie-resolved' : 'ok' };
275
+ }
276
+
277
+ function projectionTieScore(projection, preferredDistance) {
278
+ if (!Number.isFinite(preferredDistance)) return projection.segmentId;
279
+ return Math.abs(projection.distance - preferredDistance);
280
+ }
281
+
282
+ export function queryNearbyTrackProjections(track, position, { neighborLimit = GRID_NEIGHBOR_LIMIT } = {}) {
283
+ const index = track?.queryIndex;
284
+ if (!index?.centerline?.segmentCount || !finitePoint(position)) return null;
285
+ const ids = candidateIdsFromGrid(index, index.grid, position, neighborLimit);
286
+ if (!ids.length) return [];
287
+ return ids.map((segmentId) => projectIndexedSegment(index.centerline, segmentId, position));
288
+ }
289
+
290
+ export function queryTrackSegmentsInBounds(track, bounds) {
291
+ const index = track?.queryIndex;
292
+ const grid = index?.segmentGrid ?? index?.grid;
293
+ if (!index?.centerline?.segmentCount || !grid || !finiteBounds(bounds)) return null;
294
+ const ids = candidateIdsFromGridBounds(index, grid, bounds);
295
+ if (!ids.length) return [];
296
+ return ids.map((segmentId) => segmentFromIndex(index.centerline, segmentId));
297
+ }
298
+
299
+ export function queryTrackSegmentsAlongRay(track, origin, vector, maxDistance, margin = 0) {
300
+ const index = track?.queryIndex;
301
+ if (
302
+ !index?.centerline?.segmentCount ||
303
+ !finitePoint(origin) ||
304
+ !Number.isFinite(vector?.x) ||
305
+ !Number.isFinite(vector?.y) ||
306
+ !Number.isFinite(maxDistance) ||
307
+ maxDistance < 0
308
+ ) return null;
309
+
310
+ const grid = index.segmentGrid ?? index.grid;
311
+ const scratch = ensureQueryScratch(index);
312
+ const segmentMarks = ensureScratchArray(scratch, 'raySegmentMarks', index.centerline.segmentCount);
313
+ const cellMarks = ensureScratchArray(scratch, 'rayCellMarks', grid.columns * grid.rows);
314
+ const segmentEpoch = nextScratchEpoch(scratch, 'raySegmentEpoch', segmentMarks);
315
+ const cellEpoch = nextScratchEpoch(scratch, 'rayCellEpoch', cellMarks);
316
+ const ids = scratch.raySegmentIds;
317
+ ids.length = 0;
318
+ const radius = Math.max(0, Math.ceil(Math.max(0, margin) / grid.cellSize));
319
+ const step = Math.max(grid.cellSize * 0.5, 1);
320
+ const sampleCount = Math.max(1, Math.ceil(maxDistance / step));
321
+ const visitedCells = scratch.rayVisitedCells;
322
+ visitedCells.length = 0;
323
+
324
+ const seed = seedRayTraceFromNearbyCache(index, grid, origin, vector, radius, maxDistance, segmentMarks, segmentEpoch, cellMarks, cellEpoch, ids);
325
+
326
+ for (let sample = 0; sample <= sampleCount; sample += 1) {
327
+ const distance = Math.min(maxDistance, sample * step);
328
+ const point = {
329
+ x: origin.x + vector.x * distance,
330
+ y: origin.y + vector.y * distance,
331
+ };
332
+ const center = gridCellForPoint(grid, point, false);
333
+ if (!center) continue;
334
+ for (let row = center.row - radius; row <= center.row + radius; row += 1) {
335
+ for (let column = center.column - radius; column <= center.column + radius; column += 1) {
336
+ if (column < 0 || row < 0 || column >= grid.columns || row >= grid.rows) continue;
337
+ const cellIndex = row * grid.columns + column;
338
+ if (cellMarks[cellIndex] === cellEpoch) continue;
339
+ cellMarks[cellIndex] = cellEpoch;
340
+ visitedCells.push(cellIndex);
341
+ const cell = grid.cells[cellIndex];
342
+ if (!cell) continue;
343
+ for (const segmentId of cell) {
344
+ if (segmentMarks[segmentId] === segmentEpoch) continue;
345
+ segmentMarks[segmentId] = segmentEpoch;
346
+ ids.push(segmentId);
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ storeRayTraceCache(index, grid, origin, vector, radius, maxDistance, ids, visitedCells, seed);
353
+ if (!ids.length) return [];
354
+ return ids.map((segmentId) => segmentFromIndex(index.centerline, segmentId));
355
+ }
356
+
357
+ export function attachTrackQueryIndex(track, queryIndex) {
358
+ Object.defineProperty(track, 'queryIndex', {
359
+ configurable: true,
360
+ enumerable: false,
361
+ value: queryIndex,
362
+ writable: false,
363
+ });
364
+ return track;
365
+ }
366
+
367
+ export function forkTrackQueryIndex(sourceIndex) {
368
+ if (!sourceIndex || typeof sourceIndex !== 'object') return null;
369
+ return {
370
+ ...sourceIndex,
371
+ stats: createQueryStats(),
372
+ queryScratch: createQueryScratch(sourceIndex),
373
+ };
374
+ }
375
+
376
+ export function resetTrackQueryStats(track) {
377
+ const stats = track?.queryIndex?.stats;
378
+ if (!stats) return null;
379
+ Object.assign(stats, createQueryStats());
380
+ return stats;
381
+ }
382
+
383
+ export function snapshotTrackQueryStats(track) {
384
+ const stats = track?.queryIndex?.stats;
385
+ return stats ? JSON.parse(JSON.stringify(stats)) : null;
386
+ }
387
+
388
+ function createTrackBands(track) {
389
+ const trackEdge = track.width / 2;
390
+ const kerbEdge = trackEdge + (track.kerbWidth ?? 0);
391
+ const gravelEdge = kerbEdge + (track.gravelWidth ?? 0);
392
+ const runoffEdge = gravelEdge + (track.runoffWidth ?? 0);
393
+ const barrierInnerFace = runoffEdge - (track.barrierWidth ?? 0) / 2;
394
+ return {
395
+ trackEdge,
396
+ kerbEdge,
397
+ gravelEdge,
398
+ runoffEdge,
399
+ barrierInnerFace,
400
+ };
401
+ }
402
+
403
+ function createCenterlineSegments(samples, segmentCount) {
404
+ const startX = new Float64Array(segmentCount);
405
+ const startY = new Float64Array(segmentCount);
406
+ const endX = new Float64Array(segmentCount);
407
+ const endY = new Float64Array(segmentCount);
408
+ const startDistance = new Float64Array(segmentCount);
409
+ const endDistance = new Float64Array(segmentCount);
410
+ const heading = new Float64Array(segmentCount);
411
+ const normalX = new Float64Array(segmentCount);
412
+ const normalY = new Float64Array(segmentCount);
413
+ const endNormalX = new Float64Array(segmentCount);
414
+ const endNormalY = new Float64Array(segmentCount);
415
+ const curvature = new Float64Array(segmentCount);
416
+ const minX = new Float64Array(segmentCount);
417
+ const maxX = new Float64Array(segmentCount);
418
+ const minY = new Float64Array(segmentCount);
419
+ const maxY = new Float64Array(segmentCount);
420
+
421
+ for (let id = 0; id < segmentCount; id += 1) {
422
+ const start = samples[id];
423
+ const end = samples[id + 1];
424
+ startX[id] = start.x;
425
+ startY[id] = start.y;
426
+ endX[id] = end.x;
427
+ endY[id] = end.y;
428
+ startDistance[id] = start.distance;
429
+ endDistance[id] = end.distance;
430
+ heading[id] = start.heading;
431
+ normalX[id] = start.normalX;
432
+ normalY[id] = start.normalY;
433
+ endNormalX[id] = end.normalX;
434
+ endNormalY[id] = end.normalY;
435
+ curvature[id] = start.curvature ?? 0;
436
+ minX[id] = Math.min(start.x, end.x);
437
+ maxX[id] = Math.max(start.x, end.x);
438
+ minY[id] = Math.min(start.y, end.y);
439
+ maxY[id] = Math.max(start.y, end.y);
440
+ }
441
+
442
+ return {
443
+ segmentCount,
444
+ startX,
445
+ startY,
446
+ endX,
447
+ endY,
448
+ startDistance,
449
+ endDistance,
450
+ heading,
451
+ normalX,
452
+ normalY,
453
+ endNormalX,
454
+ endNormalY,
455
+ curvature,
456
+ minX,
457
+ maxX,
458
+ minY,
459
+ maxY,
460
+ };
461
+ }
462
+
463
+ function createArcBuckets(totalLength, count) {
464
+ return {
465
+ count,
466
+ totalLength,
467
+ bucketLength: totalLength / count,
468
+ buckets: Array.from({ length: count }, () => []),
469
+ };
470
+ }
471
+
472
+ function insertSegmentIntoArcBuckets(arcBuckets, centerline, segmentId) {
473
+ if (!Number.isFinite(arcBuckets.bucketLength) || arcBuckets.bucketLength <= 0) return;
474
+ const start = Math.floor(centerline.startDistance[segmentId] / arcBuckets.bucketLength);
475
+ const end = Math.floor(centerline.endDistance[segmentId] / arcBuckets.bucketLength);
476
+ for (let bucket = start; bucket <= end; bucket += 1) {
477
+ arcBuckets.buckets[((bucket % arcBuckets.count) + arcBuckets.count) % arcBuckets.count].push(segmentId);
478
+ }
479
+ }
480
+
481
+ function expandedSampleBounds(samples, expansion) {
482
+ const bounds = samples.reduce((current, point) => ({
483
+ minX: Math.min(current.minX, point.x),
484
+ maxX: Math.max(current.maxX, point.x),
485
+ minY: Math.min(current.minY, point.y),
486
+ maxY: Math.max(current.maxY, point.y),
487
+ }), { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity });
488
+ return {
489
+ minX: bounds.minX - expansion,
490
+ maxX: bounds.maxX + expansion,
491
+ minY: bounds.minY - expansion,
492
+ maxY: bounds.maxY + expansion,
493
+ };
494
+ }
495
+
496
+ function insertSegmentIntoGrid(grid, centerline, segmentId, expansion) {
497
+ insertIdIntoGridBounds(grid, segmentId, {
498
+ minX: centerline.minX[segmentId] - expansion,
499
+ maxX: centerline.maxX[segmentId] + expansion,
500
+ minY: centerline.minY[segmentId] - expansion,
501
+ maxY: centerline.maxY[segmentId] + expansion,
502
+ });
503
+ }
504
+
505
+ function candidateIdsFromArcBuckets(index, distanceAlong, radius) {
506
+ const arcBuckets = index?.arcBuckets;
507
+ if (!arcBuckets?.count || !Number.isFinite(distanceAlong)) return [];
508
+ const scratch = ensureQueryScratch(index);
509
+ const segmentMarks = ensureScratchArray(scratch, 'candidateMarks', index.centerline.segmentCount);
510
+ const candidateEpoch = nextScratchEpoch(scratch, 'candidateEpoch', segmentMarks);
511
+ const wrapped = wrapDistance(distanceAlong, arcBuckets.totalLength);
512
+ const center = Math.floor(wrapped / arcBuckets.bucketLength);
513
+ const ids = [];
514
+ for (let offset = -radius; offset <= radius; offset += 1) {
515
+ const bucket = ((center + offset) % arcBuckets.count + arcBuckets.count) % arcBuckets.count;
516
+ for (const segmentId of arcBuckets.buckets[bucket]) {
517
+ if (segmentMarks[segmentId] === candidateEpoch) continue;
518
+ segmentMarks[segmentId] = candidateEpoch;
519
+ ids.push(segmentId);
520
+ }
521
+ }
522
+ return ids;
523
+ }
524
+
525
+ function projectIndexedSegment(centerline, segmentId, position) {
526
+ const ax = centerline.startX[segmentId];
527
+ const ay = centerline.startY[segmentId];
528
+ const bx = centerline.endX[segmentId];
529
+ const by = centerline.endY[segmentId];
530
+ const dx = bx - ax;
531
+ const dy = by - ay;
532
+ const lengthSquared = dx * dx + dy * dy;
533
+ const amount = lengthSquared > 0
534
+ ? clamp(((position.x - ax) * dx + (position.y - ay) * dy) / lengthSquared, 0, 1)
535
+ : 0;
536
+ const x = ax + dx * amount;
537
+ const y = ay + dy * amount;
538
+ const distance = centerline.startDistance[segmentId] +
539
+ (centerline.endDistance[segmentId] - centerline.startDistance[segmentId]) * amount;
540
+ const heading = centerline.heading[segmentId];
541
+ const normalX = centerline.normalX[segmentId];
542
+ const normalY = centerline.normalY[segmentId];
543
+ const px = position.x - x;
544
+ const py = position.y - y;
545
+ const signedOffset = px * normalX + py * normalY;
546
+
547
+ return {
548
+ segmentId,
549
+ x,
550
+ y,
551
+ distance,
552
+ heading,
553
+ normalX,
554
+ normalY,
555
+ curvature: centerline.curvature[segmentId],
556
+ signedOffset,
557
+ crossTrackError: Math.abs(signedOffset),
558
+ distanceSquared: px * px + py * py,
559
+ };
560
+ }
561
+
562
+ function segmentFromIndex(centerline, segmentId) {
563
+ return {
564
+ segmentId,
565
+ startX: centerline.startX[segmentId],
566
+ startY: centerline.startY[segmentId],
567
+ endX: centerline.endX[segmentId],
568
+ endY: centerline.endY[segmentId],
569
+ startDistance: centerline.startDistance[segmentId],
570
+ endDistance: centerline.endDistance[segmentId],
571
+ normalX: centerline.normalX[segmentId],
572
+ normalY: centerline.normalY[segmentId],
573
+ endNormalX: centerline.endNormalX[segmentId],
574
+ endNormalY: centerline.endNormalY[segmentId],
575
+ };
576
+ }
577
+
578
+ function seedRayTraceFromNearbyCache(
579
+ index,
580
+ grid,
581
+ origin,
582
+ vector,
583
+ radius,
584
+ maxDistance,
585
+ segmentMarks,
586
+ segmentEpoch,
587
+ cellMarks,
588
+ cellEpoch,
589
+ ids,
590
+ ) {
591
+ const scratch = ensureQueryScratch(index);
592
+ const traceCache = scratch.rayTraceCache;
593
+ const originCell = gridCellForPoint(grid, origin, false);
594
+ if (!originCell) return null;
595
+ const bucket = rayTraceBucket(vector);
596
+ const base = rayTraceBaseKey(grid, originCell, radius);
597
+ const reuseCandidates = [bucket, bucket - 1, bucket + 1]
598
+ .map((candidateBucket) => traceCache.get(rayTraceKey(base, candidateBucket)))
599
+ .filter(Boolean)
600
+ .filter((entry) => entry.maxDistance >= maxDistance * 0.85)
601
+ .sort((a, b) => b.maxDistance - a.maxDistance);
602
+ const seed = reuseCandidates[0];
603
+ if (!seed) return { base, bucket };
604
+
605
+ for (const segmentId of seed.segmentIds) {
606
+ if (segmentMarks[segmentId] === segmentEpoch) continue;
607
+ segmentMarks[segmentId] = segmentEpoch;
608
+ ids.push(segmentId);
609
+ }
610
+ for (const cellId of seed.cellIds) {
611
+ cellMarks[cellId] = cellEpoch;
612
+ }
613
+ return { base, bucket };
614
+ }
615
+
616
+ function storeRayTraceCache(index, grid, origin, vector, radius, maxDistance, segmentIds, cellIds, seed) {
617
+ const scratch = ensureQueryScratch(index);
618
+ const traceCache = scratch.rayTraceCache;
619
+ const originCell = gridCellForPoint(grid, origin, false);
620
+ if (!originCell) return;
621
+ const bucket = seed?.bucket ?? rayTraceBucket(vector);
622
+ const base = seed?.base ?? rayTraceBaseKey(grid, originCell, radius);
623
+ const key = rayTraceKey(base, bucket);
624
+ traceCache.set(key, {
625
+ maxDistance,
626
+ segmentIds: segmentIds.slice(),
627
+ cellIds: cellIds.slice(),
628
+ });
629
+ if (traceCache.size > 96) {
630
+ const oldestKey = traceCache.keys().next().value;
631
+ if (oldestKey) traceCache.delete(oldestKey);
632
+ }
633
+ }
634
+
635
+ function rayTraceBaseKey(grid, center, radius) {
636
+ return `${center.column}:${center.row}:${radius}:${grid.columns}:${grid.rows}`;
637
+ }
638
+
639
+ function rayTraceKey(base, bucket) {
640
+ return `${base}:${bucket}`;
641
+ }
642
+
643
+ function rayTraceBucket(vector) {
644
+ const angleDegrees = Math.atan2(vector.y, vector.x) * 180 / Math.PI;
645
+ return Math.round(angleDegrees / RAY_TRACE_BUCKET_DEGREES);
646
+ }
647
+
648
+ function finitePoint(point) {
649
+ return Number.isFinite(point?.x) && Number.isFinite(point?.y);
650
+ }
651
+
652
+ function finiteBounds(bounds) {
653
+ return Number.isFinite(bounds?.minX) &&
654
+ Number.isFinite(bounds?.maxX) &&
655
+ Number.isFinite(bounds?.minY) &&
656
+ Number.isFinite(bounds?.maxY) &&
657
+ bounds.minX <= bounds.maxX &&
658
+ bounds.minY <= bounds.maxY;
659
+ }