@inventure71/paddockjs 2.0.1 → 4.0.0

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 (136) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +84 -512
  4. package/docs/architecture.md +38 -24
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/composable-layouts.md +77 -0
  7. package/docs/concepts.md +2 -2
  8. package/docs/custom_model_controller.md +6 -6
  9. package/docs/data-helpers.md +50 -0
  10. package/docs/data_contract.md +104 -27
  11. package/docs/getting-started.md +75 -0
  12. package/docs/headless-environment.md +68 -0
  13. package/docs/index.md +18 -4
  14. package/docs/learnings.md +6 -6
  15. package/docs/rules.md +12 -8
  16. package/docs/sense_contract.md +1 -1
  17. package/docs/system_specs.md +71 -21
  18. package/docs/theming.md +64 -0
  19. package/docs/training.md +26 -19
  20. package/docs/troubleshooting.md +76 -0
  21. package/docs/upgrading-hosts-to-3.md +42 -0
  22. package/examples/python/README.md +6 -0
  23. package/examples/python/base_policy_server.py +19 -0
  24. package/package.json +10 -3
  25. package/src/api/PaddockSimulatorController.js +150 -24
  26. package/src/api/runtimeTheme.js +59 -0
  27. package/src/app/F1SimulatorApp.js +213 -29
  28. package/src/app/banners/raceDataBannerController.js +71 -36
  29. package/src/app/banners/stewardMessageController.js +5 -4
  30. package/src/app/camera/cameraConstants.js +2 -0
  31. package/src/app/camera/cameraController.js +86 -29
  32. package/src/app/camera/cameraGeometry.js +12 -1
  33. package/src/app/domBindings.js +89 -9
  34. package/src/app/layoutScheduler.js +27 -0
  35. package/src/app/layoutSupport.js +165 -0
  36. package/src/app/raceOverlayClearanceSupport.js +158 -0
  37. package/src/app/readouts/carOverviewRenderer.js +16 -8
  38. package/src/app/readouts/controlStateRenderer.js +1 -1
  39. package/src/app/readouts/raceStatusRenderer.js +30 -21
  40. package/src/app/readouts/readoutFormatters.js +2 -0
  41. package/src/app/readouts/telemetryRenderer.js +17 -8
  42. package/src/app/readouts/timingTowerRenderer.js +262 -49
  43. package/src/app/runtime/frameLoop.js +5 -1
  44. package/src/app/runtime/runtimeVisibility.js +66 -5
  45. package/src/config/cssValues.js +16 -0
  46. package/src/config/defaultOptions.js +58 -38
  47. package/src/config/themeOptions.js +830 -0
  48. package/src/config/timingGapMode.js +13 -0
  49. package/src/config/urlOptions.js +25 -0
  50. package/src/data/championship.js +14 -9
  51. package/src/data/index.d.ts +212 -0
  52. package/src/data/index.js +14 -0
  53. package/src/environment/controllerLoop.js +17 -3
  54. package/src/environment/episode.js +11 -1
  55. package/src/environment/index.d.ts +1 -2
  56. package/src/environment/metrics.js +59 -16
  57. package/src/environment/observationVector.js +50 -33
  58. package/src/environment/observations.js +89 -20
  59. package/src/environment/options.js +1 -1
  60. package/src/environment/runtime.js +44 -11
  61. package/src/environment/scenarios.js +1 -1
  62. package/src/environment/sensors/carRays.js +48 -37
  63. package/src/environment/sensors/index.js +501 -97
  64. package/src/environment/sensors/indexedRayBands.js +134 -46
  65. package/src/environment/sensors/rayBandTrace.js +1171 -0
  66. package/src/environment/sensors/rayChannels.js +108 -0
  67. package/src/environment/sensors/rayConfig.js +71 -25
  68. package/src/environment/sensors/rayGeometry.js +23 -17
  69. package/src/environment/sensors/sensorTargets.js +67 -1
  70. package/src/environment/sensors/surfaceRays.js +167 -118
  71. package/src/environment/sensors/trackRays.js +157 -48
  72. package/src/index.d.ts +153 -9
  73. package/src/index.js +52 -3
  74. package/src/simulation/collisionGeometry.js +260 -96
  75. package/src/simulation/driver/driverController.js +1 -1
  76. package/src/simulation/driver/edgeRecovery.js +1 -1
  77. package/src/simulation/driver/racingControls.js +2 -2
  78. package/src/simulation/driver/rejoinControls.js +2 -2
  79. package/src/simulation/pit/pitFlow.js +21 -9
  80. package/src/simulation/pit/pitKinematics.js +2 -2
  81. package/src/simulation/pit/pitQueue.js +5 -3
  82. package/src/simulation/pit/pitRouteMovement.js +9 -4
  83. package/src/simulation/pit/pitRouting.js +79 -46
  84. package/src/simulation/pit/pitState.js +1 -0
  85. package/src/simulation/pit/pitTireService.js +7 -3
  86. package/src/simulation/race/raceFinish.js +25 -8
  87. package/src/simulation/race/raceOrder.js +189 -32
  88. package/src/simulation/race/raceProgress.js +129 -37
  89. package/src/simulation/race/raceSetup.js +4 -6
  90. package/src/simulation/race/raceStep.js +26 -14
  91. package/src/simulation/race/safetyCar.js +1 -1
  92. package/src/simulation/race/stalledDnf.js +1 -1
  93. package/src/simulation/raceSimulation.js +20 -17
  94. package/src/simulation/rules/collisionSteward.js +40 -23
  95. package/src/simulation/rules/rulesReview.js +77 -56
  96. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  97. package/src/simulation/timing/drsTiming.js +143 -30
  98. package/src/simulation/timing/gapEstimation.js +25 -3
  99. package/src/simulation/timing/lapTelemetry.js +138 -40
  100. package/src/simulation/timing/raceTiming.js +5 -1
  101. package/src/simulation/timing/sectorPerformance.js +104 -26
  102. package/src/simulation/timing/timingHistory.js +229 -14
  103. package/src/simulation/timing/timingLines.js +65 -7
  104. package/src/simulation/track/pitLaneLayout.js +30 -3
  105. package/src/simulation/track/pitLaneState.js +218 -19
  106. package/src/simulation/track/spatialQueries.js +66 -33
  107. package/src/simulation/track/trackMath.js +91 -24
  108. package/src/simulation/track/trackModel.js +2 -0
  109. package/src/simulation/track/trackQueryIndex.js +1468 -189
  110. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  111. package/src/simulation/track/trackQueryScratch.js +7 -0
  112. package/src/simulation/track/trackQueryStats.js +25 -0
  113. package/src/simulation/track/trackSnapshotJson.js +139 -0
  114. package/src/simulation/track/trackStatePolicy.js +249 -2
  115. package/src/simulation/vehicle/contactResolution.js +99 -62
  116. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  117. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  118. package/src/simulation/vehicle/runoffResponse.js +54 -29
  119. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  120. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  121. package/src/simulation/vehicle/vehiclePhysics.js +45 -30
  122. package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
  123. package/src/simulation/vehicle/vehicleState.js +7 -1
  124. package/src/simulation/vehicle/wheelSurface.js +807 -92
  125. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  126. package/src/styles.css +1568 -519
  127. package/src/ui/bannerTemplates.js +38 -28
  128. package/src/ui/cameraControlsTemplate.js +16 -5
  129. package/src/ui/carOverviewTemplate.js +12 -5
  130. package/src/ui/raceCanvasTemplate.js +31 -8
  131. package/src/ui/raceControlsTemplate.js +27 -10
  132. package/src/ui/shellTemplate.js +3 -1
  133. package/src/ui/telemetryDrawerTemplate.js +23 -10
  134. package/src/ui/telemetryTemplates.js +58 -32
  135. package/src/ui/templateUtils.js +48 -0
  136. package/src/ui/timingTowerTemplate.js +29 -12
@@ -12,11 +12,21 @@ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
12
12
  import { pointOnRay } from './rayGeometry.js';
13
13
  import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
14
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
15
  export function requestedSurfaceChannels(channels = []) {
19
- return channels.filter((channel) => SURFACE_CHANNELS.has(channel));
16
+ const requested = [];
17
+ let hasKerb = false;
18
+ let hasIllegalSurface = false;
19
+ for (let index = 0; index < channels.length; index += 1) {
20
+ const channel = channels[index];
21
+ if (channel === 'kerb' && !hasKerb) {
22
+ requested.push(channel);
23
+ hasKerb = true;
24
+ } else if (channel === 'illegalSurface' && !hasIllegalSurface) {
25
+ requested.push(channel);
26
+ hasIllegalSurface = true;
27
+ }
28
+ }
29
+ return requested;
20
30
  }
21
31
 
22
32
  export function createSurfaceMiss(lengthMeters) {
@@ -38,45 +48,42 @@ export function estimateSurfaceHits(
38
48
  { precision = 'driver', sharedRayQuery = null } = {},
39
49
  ) {
40
50
  const requested = requestedSurfaceChannels(channels);
41
- const misses = Object.fromEntries(requested.map((channel) => [channel, createSurfaceMiss(ray.lengthMeters)]));
51
+ const hits = prepareSurfaceHitContainer(sharedRayQuery, ray.lengthMeters);
42
52
  if (!requested.length || !Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
43
- return misses;
53
+ return hits;
44
54
  }
45
55
 
46
56
  const originState = context?.precision === precision
47
57
  ? context.originState
48
58
  : 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);
59
+ writeSurfaceHitsAtOrigin(hits, requested, originState);
60
+ const indexedHits = estimateIndexedSurfaceHits({
61
+ car,
62
+ track: snapshot.track,
63
+ ray,
64
+ origin,
65
+ vector,
66
+ requested,
67
+ originState,
68
+ precision,
69
+ sharedRayQuery,
70
+ hits,
71
+ });
72
+ if (indexedHits) return indexedHits;
73
+ const analyticHits = estimateAnalyticSurfaceHits({
74
+ car,
75
+ track: snapshot.track,
76
+ ray,
77
+ vector,
78
+ requested,
79
+ originState,
80
+ hits,
78
81
  });
79
- if (pending.size === 0) return misses;
82
+ if (analyticHits) return analyticHits;
83
+
84
+ let pendingKerb = requestedIncludes(requested, 'kerb') && !matchesSurfaceChannel('kerb', originState);
85
+ let pendingIllegalSurface = requestedIncludes(requested, 'illegalSurface') && !matchesSurfaceChannel('illegalSurface', originState);
86
+ if (!pendingKerb && !pendingIllegalSurface) return hits;
80
87
  const maxDistance = metersToSimUnits(ray.lengthMeters);
81
88
  const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
82
89
  let previousDistance = 0;
@@ -84,60 +91,78 @@ export function estimateSurfaceHits(
84
91
 
85
92
  for (let distance = 0; distance <= maxDistance; distance += step) {
86
93
  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;
94
+ if (pendingKerb && matchesSurfaceChannel('kerb', state)) {
89
95
  const hitDistance = previousState
90
- ? refineSurfaceTransition(snapshot.track, car, origin, vector, car.progress, previousDistance, distance, channel, refineStepsForPrecision(precision))
96
+ ? refineSurfaceTransition(snapshot.track, car, origin, vector, car.progress, previousDistance, distance, 'kerb', refineStepsForPrecision(precision))
91
97
  : distance;
92
- misses[channel] = {
93
- hit: true,
94
- distanceMeters: simUnitsToMeters(hitDistance),
95
- surface: state.surface ?? null,
96
- };
97
- pending.delete(channel);
98
+ writeSurfaceHit(hits.kerb, simUnitsToMeters(hitDistance), state.surface ?? null);
99
+ pendingKerb = false;
98
100
  }
99
- if (pending.size === 0) break;
101
+ if (pendingIllegalSurface && matchesSurfaceChannel('illegalSurface', state)) {
102
+ const hitDistance = previousState
103
+ ? refineSurfaceTransition(snapshot.track, car, origin, vector, car.progress, previousDistance, distance, 'illegalSurface', refineStepsForPrecision(precision))
104
+ : distance;
105
+ writeSurfaceHit(hits.illegalSurface, simUnitsToMeters(hitDistance), state.surface ?? null);
106
+ pendingIllegalSurface = false;
107
+ }
108
+ if (!pendingKerb && !pendingIllegalSurface) break;
100
109
  previousDistance = distance;
101
110
  previousState = state;
102
111
  }
103
112
 
104
- return misses;
113
+ return hits;
105
114
  }
106
115
 
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
- });
116
+ function prepareSurfaceHitContainer(cache, lengthMeters) {
117
+ const hits = cache
118
+ ? (cache.surfaceHits ?? {})
119
+ : {};
120
+ if (cache) cache.surfaceHits = hits;
121
+ hits.kerb = writeSurfaceMiss(hits.kerb ?? {}, lengthMeters);
122
+ hits.illegalSurface = writeSurfaceMiss(hits.illegalSurface ?? {}, lengthMeters);
117
123
  return hits;
118
124
  }
119
125
 
120
- function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState }) {
126
+ function writeSurfaceMiss(target, lengthMeters) {
127
+ target.hit = false;
128
+ target.distanceMeters = lengthMeters;
129
+ target.surface = null;
130
+ return target;
131
+ }
132
+
133
+ function writeSurfaceHit(target, distanceMeters, surface) {
134
+ target.hit = true;
135
+ target.distanceMeters = distanceMeters;
136
+ target.surface = surface ?? null;
137
+ return target;
138
+ }
139
+
140
+ function writeSurfaceHitsAtOrigin(hits, requested, originState) {
141
+ for (let index = 0; index < requested.length; index += 1) {
142
+ const channel = requested[index];
143
+ if (!matchesSurfaceChannel(channel, originState)) continue;
144
+ writeSurfaceHit(hits[channel], 0, originState.surface ?? null);
145
+ }
146
+ }
147
+
148
+ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState, hits }) {
121
149
  if (!originState || car.inPitLane || originState.inPitLane) return null;
122
150
  if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
123
151
  if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
124
152
  if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return null;
125
153
 
126
154
  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
- ]));
155
+ if (Math.abs(lateral) < 0.08) return hits;
131
156
 
132
157
  const trackHalfWidth = track.width / 2;
133
158
  const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
134
159
  const maxDistance = metersToSimUnits(ray.lengthMeters);
135
160
  const offset = originState.signedOffset ?? car.signedOffset ?? 0;
136
- const hits = {};
137
161
 
138
- requested.forEach((channel) => {
162
+ for (let index = 0; index < requested.length; index += 1) {
163
+ const channel = requested[index];
139
164
  if (channel === 'kerb') {
140
- hits[channel] = hitAbsOffsetBand({
165
+ hitAbsOffsetBand(hits.kerb, {
141
166
  offset,
142
167
  lateral,
143
168
  minAbsOffset: trackHalfWidth,
@@ -147,7 +172,7 @@ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, origi
147
172
  surface: 'kerb',
148
173
  });
149
174
  } else if (channel === 'illegalSurface') {
150
- hits[channel] = hitAbsOffsetBand({
175
+ hitAbsOffsetBand(hits.illegalSurface, {
151
176
  offset,
152
177
  lateral,
153
178
  minAbsOffset: kerbOuter,
@@ -157,15 +182,14 @@ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, origi
157
182
  surface: surfaceBeyondKerb(track, offset, lateral, maxDistance),
158
183
  });
159
184
  }
160
- });
185
+ }
161
186
 
162
187
  return hits;
163
188
  }
164
189
 
165
- function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery }) {
190
+ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery, hits }) {
166
191
  if (!originState || car.inPitLane || originState.inPitLane) return null;
167
192
  if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
168
- if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
169
193
 
170
194
  const trackHalfWidth = track.width / 2;
171
195
  const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
@@ -180,19 +204,27 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
180
204
  sharedRayQuery,
181
205
  );
182
206
  if (!boundaries.available) return null;
183
- const hits = {};
184
207
 
185
- for (const channel of requested) {
208
+ for (let index = 0; index < requested.length; index += 1) {
209
+ const channel = requested[index];
186
210
  if (matchesSurfaceChannel(channel, originState)) {
187
- hits[channel] = { hit: true, distanceMeters: 0, surface: originState.surface ?? null };
211
+ writeSurfaceHit(hits[channel], 0, originState.surface ?? null);
188
212
  continue;
189
213
  }
190
214
 
191
215
  const boundaryDistance = channel === 'kerb'
192
- ? minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
216
+ ? minFinite2(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
193
217
  : boundaries.kerbOuterDistance;
194
- if (boundaryDistance == null) return null;
195
- const hitDistance = precision === 'debug' || usesMainTrackOnlyRays(car)
218
+ if (boundaryDistance == null) {
219
+ if (usesMainTrackOnlyRays(car) && channel === 'illegalSurface') {
220
+ writeSurfaceMiss(hits[channel], ray.lengthMeters);
221
+ continue;
222
+ }
223
+ if (canSampleRecoverySurface(track, originState, vector)) return null;
224
+ writeSurfaceMiss(hits[channel], ray.lengthMeters);
225
+ continue;
226
+ }
227
+ const hitDistance = usesMainTrackOnlyRays(car) || refineStepsForPrecision(precision) <= 0 || precision === 'debug'
196
228
  ? boundaryDistance
197
229
  : sampledIndexedSurfaceDistance({
198
230
  track,
@@ -206,13 +238,13 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
206
238
  precision,
207
239
  });
208
240
  if (hitDistance == null) return null;
209
- hits[channel] = {
210
- hit: true,
211
- distanceMeters: simUnitsToMeters(hitDistance),
212
- surface: channel === 'kerb'
241
+ writeSurfaceHit(
242
+ hits[channel],
243
+ simUnitsToMeters(hitDistance),
244
+ channel === 'kerb'
213
245
  ? 'kerb'
214
246
  : surfaceBeyondKerb(track, originState.signedOffset ?? 0, lateral, metersToSimUnits(ray.lengthMeters)),
215
- };
247
+ );
216
248
  }
217
249
 
218
250
  return hits;
@@ -244,35 +276,28 @@ function usesMainTrackOnlyRays(car) {
244
276
  return car?.interaction?.profile === 'batch-training';
245
277
  }
246
278
 
247
- function hitAbsOffsetBand({ offset, lateral, minAbsOffset, maxAbsOffset, maxDistance, lengthMeters, surface }) {
279
+ function hitAbsOffsetBand(target, { offset, lateral, minAbsOffset, maxAbsOffset, maxDistance, lengthMeters, surface }) {
248
280
  const currentAbs = Math.abs(offset);
249
281
  if (currentAbs >= minAbsOffset && currentAbs <= maxAbsOffset) {
250
- return { hit: true, distanceMeters: 0, surface };
282
+ return writeSurfaceHit(target, 0, surface);
251
283
  }
252
284
 
253
- const candidates = [
254
- distanceToOffset(minAbsOffset, offset, lateral),
255
- distanceToOffset(-minAbsOffset, offset, lateral),
256
- ].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance);
257
-
285
+ let distance = Infinity;
286
+ distance = nearestValidBandDistance(distance, distanceToOffset(minAbsOffset, offset, lateral), offset, lateral, minAbsOffset, maxAbsOffset, maxDistance);
287
+ distance = nearestValidBandDistance(distance, distanceToOffset(-minAbsOffset, offset, lateral), offset, lateral, minAbsOffset, maxAbsOffset, maxDistance);
258
288
  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
- );
289
+ distance = nearestValidBandDistance(distance, distanceToOffset(maxAbsOffset, offset, lateral), offset, lateral, minAbsOffset, maxAbsOffset, maxDistance);
290
+ distance = nearestValidBandDistance(distance, distanceToOffset(-maxAbsOffset, offset, lateral), offset, lateral, minAbsOffset, maxAbsOffset, maxDistance);
265
291
  }
266
292
 
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];
293
+ if (!Number.isFinite(distance)) return writeSurfaceMiss(target, lengthMeters);
294
+ return writeSurfaceHit(target, simUnitsToMeters(distance), surface);
295
+ }
273
296
 
274
- if (!Number.isFinite(distance)) return createSurfaceMiss(lengthMeters);
275
- return { hit: true, distanceMeters: simUnitsToMeters(distance), surface };
297
+ function nearestValidBandDistance(current, candidate, offset, lateral, minAbsOffset, maxAbsOffset, maxDistance) {
298
+ if (!Number.isFinite(candidate) || candidate < 0 || candidate > maxDistance || candidate >= current) return current;
299
+ const nextAbs = Math.abs(offset + lateral * candidate);
300
+ return nextAbs >= minAbsOffset - 1e-6 && nextAbs <= maxAbsOffset + 1e-6 ? candidate : current;
276
301
  }
277
302
 
278
303
  function distanceToOffset(targetOffset, offset, lateral) {
@@ -281,12 +306,16 @@ function distanceToOffset(targetOffset, offset, lateral) {
281
306
 
282
307
  function surfaceBeyondKerb(track, offset, lateral, maxDistance) {
283
308
  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
- );
309
+ const kerbOuter = track.width / 2 + (track.kerbWidth ?? 0);
310
+ const positiveDistance = distanceToOffset(kerbOuter, offset, lateral);
311
+ const negativeDistance = distanceToOffset(-kerbOuter, offset, lateral);
312
+ let hitDistance = Infinity;
313
+ if (Number.isFinite(positiveDistance) && positiveDistance >= 0 && positiveDistance <= maxDistance) {
314
+ hitDistance = positiveDistance;
315
+ }
316
+ if (Number.isFinite(negativeDistance) && negativeDistance >= 0 && negativeDistance <= maxDistance && negativeDistance < hitDistance) {
317
+ hitDistance = negativeDistance;
318
+ }
290
319
  const hitAbs = Math.abs(offset + lateral * hitDistance);
291
320
  return hitAbs <= gravelOuter ? 'gravel' : 'grass';
292
321
  }
@@ -337,24 +366,44 @@ function refineStepsForPrecision(precision) {
337
366
 
338
367
  function matchesSurfaceChannel(channel, state) {
339
368
  if (channel === 'kerb') return state.surface === 'kerb';
340
- if (channel === 'illegalSurface') return !LEGAL_SURFACES.has(state.surface) && state.surface !== 'barrier';
369
+ if (channel === 'illegalSurface') return !isLegalSurface(state.surface) && state.surface !== 'barrier';
341
370
  return false;
342
371
  }
343
372
 
373
+ function requestedIncludes(requested, channel) {
374
+ for (let index = 0; index < requested.length; index += 1) {
375
+ if (requested[index] === channel) return true;
376
+ }
377
+ return false;
378
+ }
379
+
380
+ function isLegalSurface(surface) {
381
+ return surface === 'track' ||
382
+ surface === 'kerb' ||
383
+ surface === 'pit-entry' ||
384
+ surface === 'pit-lane' ||
385
+ surface === 'pit-exit' ||
386
+ surface === 'pit-box';
387
+ }
388
+
389
+ function canSampleRecoverySurface(track, originState, vector) {
390
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
391
+ const kerbOuter = track.width / 2 + (track.kerbWidth ?? 0);
392
+ const offset = originState.signedOffset ?? 0;
393
+ if (Math.abs(offset) <= kerbOuter) return false;
394
+ const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
395
+ if (Math.abs(lateral) < 0.05) return false;
396
+ return offset > 0 ? lateral < 0 : lateral > 0;
397
+ }
398
+
344
399
  function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
345
- const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
346
400
  return nearestTrackState(track, point, progressHint, {
347
401
  allowPitOverride: pitOverrideAllowedForCar(car),
348
- indexMode: usesSampledDriverContract(car, precision) ? 'legacy' : useBatchTrainingIndexMode ? 'sample' : undefined,
349
- hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
350
402
  });
351
403
  }
352
404
 
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;
405
+ function minFinite2(first, second) {
406
+ if (!Number.isFinite(first)) return Number.isFinite(second) ? second : null;
407
+ if (!Number.isFinite(second)) return first;
408
+ return first <= second ? first : second;
360
409
  }