@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,360 @@
1
+ import { nearestTrackState } from '../../simulation/track/trackModel.js';
2
+ import { pitOverrideAllowedForCar } from '../../simulation/track/trackStatePolicy.js';
3
+ import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
4
+ import {
5
+ ANALYTIC_TRACK_RAY_MAX_CURVATURE,
6
+ DRIVER_RAY_REFINE_STEPS,
7
+ PIT_CONNECTOR_RAY_FALLBACK_METERS,
8
+ TRACK_RAY_REFINE_STEPS,
9
+ TRACK_RAY_STEP_METERS,
10
+ } from './rayDefaults.js';
11
+ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
12
+ import { pointOnRay } from './rayGeometry.js';
13
+ import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
14
+
15
+ const LEGAL_SURFACES = new Set(['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box']);
16
+ const SURFACE_CHANNELS = new Set(['kerb', 'illegalSurface']);
17
+
18
+ export function requestedSurfaceChannels(channels = []) {
19
+ return channels.filter((channel) => SURFACE_CHANNELS.has(channel));
20
+ }
21
+
22
+ export function createSurfaceMiss(lengthMeters) {
23
+ return {
24
+ hit: false,
25
+ distanceMeters: lengthMeters,
26
+ surface: null,
27
+ };
28
+ }
29
+
30
+ export function estimateSurfaceHits(
31
+ car,
32
+ snapshot,
33
+ ray,
34
+ origin,
35
+ vector,
36
+ channels,
37
+ context = null,
38
+ { precision = 'driver', sharedRayQuery = null } = {},
39
+ ) {
40
+ const requested = requestedSurfaceChannels(channels);
41
+ const misses = Object.fromEntries(requested.map((channel) => [channel, createSurfaceMiss(ray.lengthMeters)]));
42
+ if (!requested.length || !Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
43
+ return misses;
44
+ }
45
+
46
+ const originState = context?.precision === precision
47
+ ? context.originState
48
+ : nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
49
+ const originHits = surfaceHitsAtOrigin(requested, originState);
50
+ if (!usesSampledDriverContract(car, precision)) {
51
+ const indexedHits = estimateIndexedSurfaceHits({
52
+ car,
53
+ track: snapshot.track,
54
+ ray,
55
+ origin,
56
+ vector,
57
+ requested,
58
+ originState,
59
+ precision,
60
+ sharedRayQuery,
61
+ });
62
+ if (indexedHits) return { ...misses, ...indexedHits, ...originHits };
63
+ const analyticHits = estimateAnalyticSurfaceHits({
64
+ car,
65
+ track: snapshot.track,
66
+ ray,
67
+ vector,
68
+ requested,
69
+ originState,
70
+ });
71
+ if (analyticHits) return { ...misses, ...analyticHits, ...originHits };
72
+ }
73
+
74
+ const pending = new Set(requested);
75
+ Object.keys(originHits).forEach((channel) => {
76
+ misses[channel] = originHits[channel];
77
+ pending.delete(channel);
78
+ });
79
+ if (pending.size === 0) return misses;
80
+ const maxDistance = metersToSimUnits(ray.lengthMeters);
81
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
82
+ let previousDistance = 0;
83
+ let previousState = null;
84
+
85
+ for (let distance = 0; distance <= maxDistance; distance += step) {
86
+ const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress, precision);
87
+ for (const channel of [...pending]) {
88
+ if (!matchesSurfaceChannel(channel, state)) continue;
89
+ const hitDistance = previousState
90
+ ? refineSurfaceTransition(snapshot.track, car, origin, vector, car.progress, previousDistance, distance, channel, refineStepsForPrecision(precision))
91
+ : distance;
92
+ misses[channel] = {
93
+ hit: true,
94
+ distanceMeters: simUnitsToMeters(hitDistance),
95
+ surface: state.surface ?? null,
96
+ };
97
+ pending.delete(channel);
98
+ }
99
+ if (pending.size === 0) break;
100
+ previousDistance = distance;
101
+ previousState = state;
102
+ }
103
+
104
+ return misses;
105
+ }
106
+
107
+ function surfaceHitsAtOrigin(requested, originState) {
108
+ const hits = {};
109
+ requested.forEach((channel) => {
110
+ if (!matchesSurfaceChannel(channel, originState)) return;
111
+ hits[channel] = {
112
+ hit: true,
113
+ distanceMeters: 0,
114
+ surface: originState.surface ?? null,
115
+ };
116
+ });
117
+ return hits;
118
+ }
119
+
120
+ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState }) {
121
+ if (!originState || car.inPitLane || originState.inPitLane) return null;
122
+ if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
123
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
124
+ if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return null;
125
+
126
+ const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
127
+ if (Math.abs(lateral) < 0.08) return Object.fromEntries(requested.map((channel) => [
128
+ channel,
129
+ createSurfaceMiss(ray.lengthMeters),
130
+ ]));
131
+
132
+ const trackHalfWidth = track.width / 2;
133
+ const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
134
+ const maxDistance = metersToSimUnits(ray.lengthMeters);
135
+ const offset = originState.signedOffset ?? car.signedOffset ?? 0;
136
+ const hits = {};
137
+
138
+ requested.forEach((channel) => {
139
+ if (channel === 'kerb') {
140
+ hits[channel] = hitAbsOffsetBand({
141
+ offset,
142
+ lateral,
143
+ minAbsOffset: trackHalfWidth,
144
+ maxAbsOffset: kerbOuter,
145
+ maxDistance,
146
+ lengthMeters: ray.lengthMeters,
147
+ surface: 'kerb',
148
+ });
149
+ } else if (channel === 'illegalSurface') {
150
+ hits[channel] = hitAbsOffsetBand({
151
+ offset,
152
+ lateral,
153
+ minAbsOffset: kerbOuter,
154
+ maxAbsOffset: Infinity,
155
+ maxDistance,
156
+ lengthMeters: ray.lengthMeters,
157
+ surface: surfaceBeyondKerb(track, offset, lateral, maxDistance),
158
+ });
159
+ }
160
+ });
161
+
162
+ return hits;
163
+ }
164
+
165
+ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery }) {
166
+ if (!originState || car.inPitLane || originState.inPitLane) return null;
167
+ if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
168
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
169
+
170
+ const trackHalfWidth = track.width / 2;
171
+ const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
172
+ const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
173
+ const boundaries = findIndexedTrackBandBoundaries(
174
+ track,
175
+ origin,
176
+ vector,
177
+ ray.lengthMeters,
178
+ trackHalfWidth,
179
+ kerbOuter,
180
+ sharedRayQuery,
181
+ );
182
+ if (!boundaries.available) return null;
183
+ const hits = {};
184
+
185
+ for (const channel of requested) {
186
+ if (matchesSurfaceChannel(channel, originState)) {
187
+ hits[channel] = { hit: true, distanceMeters: 0, surface: originState.surface ?? null };
188
+ continue;
189
+ }
190
+
191
+ const boundaryDistance = channel === 'kerb'
192
+ ? minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
193
+ : boundaries.kerbOuterDistance;
194
+ if (boundaryDistance == null) return null;
195
+ const hitDistance = precision === 'debug' || usesMainTrackOnlyRays(car)
196
+ ? boundaryDistance
197
+ : sampledIndexedSurfaceDistance({
198
+ track,
199
+ car,
200
+ origin,
201
+ vector,
202
+ progressHint: car.progress,
203
+ distance: boundaryDistance,
204
+ maxDistance: metersToSimUnits(ray.lengthMeters),
205
+ channel,
206
+ precision,
207
+ });
208
+ if (hitDistance == null) return null;
209
+ hits[channel] = {
210
+ hit: true,
211
+ distanceMeters: simUnitsToMeters(hitDistance),
212
+ surface: channel === 'kerb'
213
+ ? 'kerb'
214
+ : surfaceBeyondKerb(track, originState.signedOffset ?? 0, lateral, metersToSimUnits(ray.lengthMeters)),
215
+ };
216
+ }
217
+
218
+ return hits;
219
+ }
220
+
221
+ function sampledIndexedSurfaceDistance({
222
+ track,
223
+ car,
224
+ origin,
225
+ vector,
226
+ progressHint,
227
+ distance,
228
+ maxDistance,
229
+ channel,
230
+ precision,
231
+ }) {
232
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
233
+ const firstIndex = Math.max(0, Math.floor(Math.max(0, distance - step * 1.5) / step));
234
+ const lastIndex = Math.ceil(Math.min(maxDistance, distance + step * 8) / step);
235
+ for (let index = firstIndex; index <= lastIndex; index += 1) {
236
+ const sampleDistance = Math.min(maxDistance, index * step);
237
+ const state = nearestRayTrackState(track, car, pointOnRay(origin, vector, sampleDistance), progressHint, precision);
238
+ if (matchesSurfaceChannel(channel, state)) return sampleDistance;
239
+ }
240
+ return null;
241
+ }
242
+
243
+ function usesMainTrackOnlyRays(car) {
244
+ return car?.interaction?.profile === 'batch-training';
245
+ }
246
+
247
+ function hitAbsOffsetBand({ offset, lateral, minAbsOffset, maxAbsOffset, maxDistance, lengthMeters, surface }) {
248
+ const currentAbs = Math.abs(offset);
249
+ if (currentAbs >= minAbsOffset && currentAbs <= maxAbsOffset) {
250
+ return { hit: true, distanceMeters: 0, surface };
251
+ }
252
+
253
+ const candidates = [
254
+ distanceToOffset(minAbsOffset, offset, lateral),
255
+ distanceToOffset(-minAbsOffset, offset, lateral),
256
+ ].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance);
257
+
258
+ if (Number.isFinite(maxAbsOffset)) {
259
+ candidates.push(
260
+ ...[
261
+ distanceToOffset(maxAbsOffset, offset, lateral),
262
+ distanceToOffset(-maxAbsOffset, offset, lateral),
263
+ ].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance),
264
+ );
265
+ }
266
+
267
+ const distance = candidates
268
+ .filter((candidate) => {
269
+ const nextAbs = Math.abs(offset + lateral * candidate);
270
+ return nextAbs >= minAbsOffset - 1e-6 && nextAbs <= maxAbsOffset + 1e-6;
271
+ })
272
+ .sort((a, b) => a - b)[0];
273
+
274
+ if (!Number.isFinite(distance)) return createSurfaceMiss(lengthMeters);
275
+ return { hit: true, distanceMeters: simUnitsToMeters(distance), surface };
276
+ }
277
+
278
+ function distanceToOffset(targetOffset, offset, lateral) {
279
+ return (targetOffset - offset) / lateral;
280
+ }
281
+
282
+ function surfaceBeyondKerb(track, offset, lateral, maxDistance) {
283
+ const gravelOuter = track.width / 2 + (track.kerbWidth ?? 0) + (track.gravelWidth ?? 0);
284
+ const hitDistance = Math.min(
285
+ ...[
286
+ distanceToOffset(track.width / 2 + (track.kerbWidth ?? 0), offset, lateral),
287
+ distanceToOffset(-track.width / 2 - (track.kerbWidth ?? 0), offset, lateral),
288
+ ].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance),
289
+ );
290
+ const hitAbs = Math.abs(offset + lateral * hitDistance);
291
+ return hitAbs <= gravelOuter ? 'gravel' : 'grass';
292
+ }
293
+
294
+ function isNearPitConnector(track, state) {
295
+ const pitLane = track.pitLane;
296
+ if (!pitLane?.enabled || !Number.isFinite(state?.distance)) return false;
297
+ const window = metersToSimUnits(PIT_CONNECTOR_RAY_FALLBACK_METERS);
298
+ const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
299
+ const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
300
+ return wrappedTrackDistance(state.distance, entryDistance, track.length) <= window ||
301
+ wrappedTrackDistance(state.distance, exitDistance, track.length) <= window;
302
+ }
303
+
304
+ function wrappedTrackDistance(first, second, totalLength) {
305
+ if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(totalLength) || totalLength <= 0) {
306
+ return Infinity;
307
+ }
308
+ const delta = Math.abs(first - second);
309
+ return Math.min(delta, totalLength - delta);
310
+ }
311
+
312
+ function refineSurfaceTransition(
313
+ track,
314
+ car,
315
+ origin,
316
+ ray,
317
+ progressHint,
318
+ lowDistance,
319
+ highDistance,
320
+ channel,
321
+ refineSteps = TRACK_RAY_REFINE_STEPS,
322
+ ) {
323
+ let low = lowDistance;
324
+ let high = highDistance;
325
+ for (let index = 0; index < refineSteps; index += 1) {
326
+ const middle = (low + high) / 2;
327
+ const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint);
328
+ if (matchesSurfaceChannel(channel, state)) high = middle;
329
+ else low = middle;
330
+ }
331
+ return high;
332
+ }
333
+
334
+ function refineStepsForPrecision(precision) {
335
+ return precision === 'debug' ? TRACK_RAY_REFINE_STEPS : DRIVER_RAY_REFINE_STEPS;
336
+ }
337
+
338
+ function matchesSurfaceChannel(channel, state) {
339
+ if (channel === 'kerb') return state.surface === 'kerb';
340
+ if (channel === 'illegalSurface') return !LEGAL_SURFACES.has(state.surface) && state.surface !== 'barrier';
341
+ return false;
342
+ }
343
+
344
+ function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
345
+ const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
346
+ return nearestTrackState(track, point, progressHint, {
347
+ allowPitOverride: pitOverrideAllowedForCar(car),
348
+ indexMode: usesSampledDriverContract(car, precision) ? 'legacy' : useBatchTrainingIndexMode ? 'sample' : undefined,
349
+ hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
350
+ });
351
+ }
352
+
353
+ function usesSampledDriverContract(car, precision) {
354
+ return precision === 'driver' && !usesMainTrackOnlyRays(car);
355
+ }
356
+
357
+ function minFinite(...values) {
358
+ const minimum = Math.min(...values.filter((value) => Number.isFinite(value)));
359
+ return Number.isFinite(minimum) ? minimum : null;
360
+ }