@inventure71/paddockjs 2.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 (62) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/INSTALL_AND_UPDATE.md +1 -1
  3. package/README.md +30 -5
  4. package/docs/architecture.md +15 -9
  5. package/docs/concepts.md +6 -4
  6. package/docs/data_contract.md +28 -15
  7. package/docs/data_model.md +1 -1
  8. package/docs/index.md +9 -0
  9. package/docs/rules.md +37 -7
  10. package/docs/sense_contract.md +59 -0
  11. package/docs/system_specs.md +56 -7
  12. package/docs/training.md +16 -10
  13. package/package.json +1 -1
  14. package/src/api/PaddockSimulatorController.js +2 -33
  15. package/src/app/BrowserExpertAdapter.js +9 -300
  16. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  17. package/src/app/F1SimulatorApp.js +1 -42
  18. package/src/config/restartOptions.js +34 -0
  19. package/src/environment/controllerLoop.js +17 -2
  20. package/src/environment/episode.js +14 -2
  21. package/src/environment/index.d.ts +16 -2
  22. package/src/environment/observationVector.js +200 -0
  23. package/src/environment/observations.js +58 -285
  24. package/src/environment/recorder.js +13 -6
  25. package/src/environment/rewards.js +13 -2
  26. package/src/environment/runtime.js +62 -14
  27. package/src/environment/scenarios.js +22 -3
  28. package/src/environment/sensors/index.js +3 -3
  29. package/src/environment/sensors/opponentRadar.js +13 -3
  30. package/src/environment/sensors/sensorTargets.js +8 -2
  31. package/src/environment/sensors/surfaceRays.js +90 -26
  32. package/src/environment/sensors/trackRays.js +185 -22
  33. package/src/environment/specs.js +78 -40
  34. package/src/index.d.ts +2 -0
  35. package/src/index.js +1 -32
  36. package/src/simulation/pit/pitFlow.js +5 -0
  37. package/src/simulation/pit/pitKinematics.js +24 -0
  38. package/src/simulation/pit/pitQueue.js +5 -0
  39. package/src/simulation/pit/pitRouteMovement.js +7 -0
  40. package/src/simulation/pit/pitService.js +2 -0
  41. package/src/simulation/pit/pitTireService.js +2 -0
  42. package/src/simulation/race/raceLifecycle.js +2 -6
  43. package/src/simulation/race/raceProgress.js +6 -1
  44. package/src/simulation/race/raceStep.js +1 -1
  45. package/src/simulation/race/redFlag.js +3 -3
  46. package/src/simulation/race/safetyCar.js +10 -4
  47. package/src/simulation/race/stalledDnf.js +8 -17
  48. package/src/simulation/raceSimulation.js +1 -0
  49. package/src/simulation/rules/rulesReview.js +3 -0
  50. package/src/simulation/snapshots/raceSnapshots.js +1 -0
  51. package/src/simulation/track/trackQueryGrid.js +94 -0
  52. package/src/simulation/track/trackQueryIndex.js +16 -302
  53. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  54. package/src/simulation/track/trackQueryScratch.js +46 -0
  55. package/src/simulation/track/trackQueryStats.js +28 -0
  56. package/src/simulation/vehicle/contactResolution.js +67 -2
  57. package/src/simulation/vehicle/runoffResponse.js +11 -12
  58. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  59. package/src/simulation/vehicle/vehiclePhysics.js +2 -1
  60. package/src/simulation/vehicle/vehicleSnapshots.js +11 -0
  61. package/src/simulation/vehicle/vehicleState.js +36 -2
  62. package/src/simulation/vehicle/wheelSurface.js +3 -1
@@ -1,7 +1,7 @@
1
1
  import { createCarRayMiss, estimateCarHit } from './carRays.js';
2
2
  import { normalizeRayOptions } from './rayConfig.js';
3
3
  import { degreesToRadians, getCarRayOrigin } from './rayGeometry.js';
4
- import { rayDetectableTargetsForSnapshot } from './sensorTargets.js';
4
+ import { isSelfCarTarget, rayDetectableTargetsForSnapshot } from './sensorTargets.js';
5
5
  import { createTrackMiss, createTrackRayContext, estimateTrackHit } from './trackRays.js';
6
6
  import { createSurfaceMiss, estimateSurfaceHits, requestedSurfaceChannels } from './surfaceRays.js';
7
7
  import { canUseBatchTrainingRayApproximation } from './rayGuards.js';
@@ -28,9 +28,9 @@ export function buildRaySensors(car, snapshot, rayOptions = {}, batchContext = n
28
28
  requestedSurfaceChannels(normalized.channels).length > 0;
29
29
  const trackContext = !usesTrackContext
30
30
  ? null
31
- : createTrackRayContext(car, snapshot, origin);
31
+ : createTrackRayContext(car, snapshot, origin, normalized.precision);
32
32
  const carTargets = normalized.channels.includes('car')
33
- ? (batchContext?.rayTargets ?? rayDetectableTargetsForSnapshot(snapshot)).filter((target) => target.id !== car.id)
33
+ ? (batchContext?.rayTargets ?? rayDetectableTargetsForSnapshot(snapshot)).filter((target) => !isSelfCarTarget(car, target))
34
34
  : [];
35
35
 
36
36
  if (canUseFastBatchTrainingRays(car, snapshot, trackContext)) {
@@ -3,12 +3,12 @@ import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
3
3
 
4
4
  export function enrichOpponentRadar(car, nearbyCars, snapshot) {
5
5
  const targetsById = new Map([
6
- ...(snapshot.cars ?? []).map((entry) => [entry.id, entry]),
7
- ...(snapshot.replayGhosts ?? []).map((entry) => [entry.id, entry]),
6
+ ...(snapshot.cars ?? []).map((entry) => [targetKey('car', entry.id), entry]),
7
+ ...(snapshot.replayGhosts ?? []).map((entry) => [targetKey('replayGhost', entry.id), entry]),
8
8
  ]);
9
9
 
10
10
  return nearbyCars.map((nearby) => {
11
- const target = targetsById.get(nearby.id);
11
+ const target = targetsById.get(targetKey(nearby.entityType ?? 'car', nearby.id));
12
12
  const derived = target ? deriveRadarFields(car, target, nearby) : fallbackRadarFields(nearby);
13
13
  return {
14
14
  ...nearby,
@@ -17,6 +17,10 @@ export function enrichOpponentRadar(car, nearbyCars, snapshot) {
17
17
  });
18
18
  }
19
19
 
20
+ function targetKey(entityType, id) {
21
+ return `${entityType}:${id}`;
22
+ }
23
+
20
24
  function deriveRadarFields(car, target, nearby) {
21
25
  const dx = (target.x ?? 0) - (car.x ?? 0);
22
26
  const dy = (target.y ?? 0) - (car.y ?? 0);
@@ -56,6 +60,12 @@ function fallbackRadarFields(nearby) {
56
60
  }
57
61
 
58
62
  function velocity(car) {
63
+ if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
64
+ return {
65
+ x: car.velocityX,
66
+ y: car.velocityY,
67
+ };
68
+ }
59
69
  const speed = Number.isFinite(car.speed) ? car.speed : kphToSimSpeed(car.speedKph ?? 0);
60
70
  const heading = car.heading ?? car.headingRadians ?? 0;
61
71
  return {
@@ -7,11 +7,11 @@ export const SENSOR_TARGET_CAR = 'car';
7
7
  export const SENSOR_TARGET_REPLAY_GHOST = 'replayGhost';
8
8
 
9
9
  export function rayDetectableTargets(self, snapshot) {
10
- return rayDetectableTargetsForSnapshot(snapshot).filter((target) => target.id !== self.id);
10
+ return rayDetectableTargetsForSnapshot(snapshot).filter((target) => !isSelfCarTarget(self, target));
11
11
  }
12
12
 
13
13
  export function nearbyDetectableTargets(self, snapshot) {
14
- return nearbyDetectableTargetsForSnapshot(snapshot).filter((target) => target.id !== self.id);
14
+ return nearbyDetectableTargetsForSnapshot(snapshot).filter((target) => !isSelfCarTarget(self, target));
15
15
  }
16
16
 
17
17
  export function rayDetectableTargetsForSnapshot(snapshot) {
@@ -37,6 +37,8 @@ function detectableCars(snapshot, predicate) {
37
37
  x: car.x,
38
38
  y: car.y,
39
39
  heading: car.heading,
40
+ velocityX: car.velocityX,
41
+ velocityY: car.velocityY,
40
42
  speedKph: car.speedKph ?? 0,
41
43
  lap: car.lap,
42
44
  order,
@@ -58,3 +60,7 @@ function detectableReplayGhosts(snapshot, sensorFlag) {
58
60
  order: carCount + index,
59
61
  }));
60
62
  }
63
+
64
+ export function isSelfCarTarget(self, target) {
65
+ return target.entityType === SENSOR_TARGET_CAR && target.id === self.id;
66
+ }
@@ -43,36 +43,47 @@ export function estimateSurfaceHits(
43
43
  return misses;
44
44
  }
45
45
 
46
- const originState = context?.originState ?? nearestRayTrackState(snapshot.track, car, origin, car.progress);
47
- const indexedHits = estimateIndexedSurfaceHits({
48
- car,
49
- track: snapshot.track,
50
- ray,
51
- origin,
52
- vector,
53
- requested,
54
- originState,
55
- sharedRayQuery,
56
- });
57
- if (indexedHits) return { ...misses, ...indexedHits };
58
- const analyticHits = estimateAnalyticSurfaceHits({
59
- car,
60
- track: snapshot.track,
61
- ray,
62
- vector,
63
- requested,
64
- originState,
65
- });
66
- if (analyticHits) return { ...misses, ...analyticHits };
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
+ }
67
73
 
68
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;
69
80
  const maxDistance = metersToSimUnits(ray.lengthMeters);
70
81
  const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
71
82
  let previousDistance = 0;
72
83
  let previousState = null;
73
84
 
74
85
  for (let distance = 0; distance <= maxDistance; distance += step) {
75
- const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress);
86
+ const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress, precision);
76
87
  for (const channel of [...pending]) {
77
88
  if (!matchesSurfaceChannel(channel, state)) continue;
78
89
  const hitDistance = previousState
@@ -93,6 +104,19 @@ export function estimateSurfaceHits(
93
104
  return misses;
94
105
  }
95
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
+
96
120
  function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState }) {
97
121
  if (!originState || car.inPitLane || originState.inPitLane) return null;
98
122
  if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
@@ -138,7 +162,7 @@ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, origi
138
162
  return hits;
139
163
  }
140
164
 
141
- function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, sharedRayQuery }) {
165
+ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery }) {
142
166
  if (!originState || car.inPitLane || originState.inPitLane) return null;
143
167
  if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
144
168
  if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
@@ -168,9 +192,23 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
168
192
  ? minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
169
193
  : boundaries.kerbOuterDistance;
170
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;
171
209
  hits[channel] = {
172
210
  hit: true,
173
- distanceMeters: simUnitsToMeters(boundaryDistance),
211
+ distanceMeters: simUnitsToMeters(hitDistance),
174
212
  surface: channel === 'kerb'
175
213
  ? 'kerb'
176
214
  : surfaceBeyondKerb(track, originState.signedOffset ?? 0, lateral, metersToSimUnits(ray.lengthMeters)),
@@ -180,6 +218,28 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
180
218
  return hits;
181
219
  }
182
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
+
183
243
  function usesMainTrackOnlyRays(car) {
184
244
  return car?.interaction?.profile === 'batch-training';
185
245
  }
@@ -281,15 +341,19 @@ function matchesSurfaceChannel(channel, state) {
281
341
  return false;
282
342
  }
283
343
 
284
- function nearestRayTrackState(track, car, point, progressHint) {
344
+ function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
285
345
  const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
286
346
  return nearestTrackState(track, point, progressHint, {
287
347
  allowPitOverride: pitOverrideAllowedForCar(car),
288
- indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
348
+ indexMode: usesSampledDriverContract(car, precision) ? 'legacy' : useBatchTrainingIndexMode ? 'sample' : undefined,
289
349
  hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
290
350
  });
291
351
  }
292
352
 
353
+ function usesSampledDriverContract(car, precision) {
354
+ return precision === 'driver' && !usesMainTrackOnlyRays(car);
355
+ }
356
+
293
357
  function minFinite(...values) {
294
358
  const minimum = Math.min(...values.filter((value) => Number.isFinite(value)));
295
359
  return Number.isFinite(minimum) ? minimum : null;
@@ -12,13 +12,14 @@ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
12
12
  import { degreesToRadians, getCarRayOrigin, getCarRayVector, pointOnRay } from './rayGeometry.js';
13
13
  import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
14
14
 
15
- export function createTrackRayContext(car, snapshot, origin) {
15
+ export function createTrackRayContext(car, snapshot, origin, precision = 'debug') {
16
16
  if (!Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
17
- return { origin, originState: null };
17
+ return { origin, originState: null, precision };
18
18
  }
19
19
  return {
20
20
  origin,
21
- originState: nearestRayTrackState(snapshot.track, car, origin, car.progress),
21
+ originState: nearestRayTrackState(snapshot.track, car, origin, car.progress, precision),
22
+ precision,
22
23
  };
23
24
  }
24
25
 
@@ -38,19 +39,36 @@ export function estimateTrackHit(
38
39
  const ray = getCarRayVector(car, angleDegrees);
39
40
  const maxDistance = metersToSimUnits(lengthMeters);
40
41
  const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
41
- const originState = context?.originState ?? nearestRayTrackState(snapshot.track, car, origin, car.progress);
42
+ const originState = context?.precision === precision
43
+ ? context.originState
44
+ : nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
42
45
  const includePitLane = Boolean(car.inPitLane || car.pitLanePart || originState.inPitLane);
43
- const indexedHit = estimateIndexedTrackHit({
44
- car,
45
- track: snapshot.track,
46
- origin,
47
- originState,
48
- ray,
49
- lengthMeters,
50
- includePitLane,
51
- sharedRayQuery,
52
- });
53
- if (indexedHit) return indexedHit;
46
+ const directionalAnalyticHit = canUseDirectionalAnalyticTrackHit({ car, track: snapshot.track, originState, ray, includePitLane })
47
+ ? estimateAnalyticMainTrackHit({
48
+ car,
49
+ track: snapshot.track,
50
+ origin,
51
+ originState,
52
+ ray,
53
+ lengthMeters,
54
+ includePitLane,
55
+ })
56
+ : null;
57
+ if (directionalAnalyticHit) return directionalAnalyticHit;
58
+ if (!usesSampledDriverContract(car, precision)) {
59
+ const indexedHit = estimateIndexedTrackHit({
60
+ car,
61
+ track: snapshot.track,
62
+ origin,
63
+ originState,
64
+ ray,
65
+ lengthMeters,
66
+ includePitLane,
67
+ precision,
68
+ sharedRayQuery,
69
+ });
70
+ if (indexedHit) return indexedHit;
71
+ }
54
72
  const analyticHit = estimateAnalyticMainTrackHit({
55
73
  car,
56
74
  track: snapshot.track,
@@ -65,7 +83,7 @@ export function estimateTrackHit(
65
83
  let previousInside = null;
66
84
 
67
85
  for (let distance = 0; distance <= maxDistance; distance += step) {
68
- const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, ray, distance), car.progress);
86
+ const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, ray, distance), car.progress, precision);
69
87
  const inside = isInsideTrackBorder(state, snapshot.track, includePitLane);
70
88
  if (previousInside == null) {
71
89
  previousInside = inside;
@@ -86,6 +104,7 @@ export function estimateTrackHit(
86
104
  kind,
87
105
  includePitLane,
88
106
  refineStepsForPrecision(precision),
107
+ precision,
89
108
  );
90
109
  return {
91
110
  hit: true,
@@ -138,7 +157,16 @@ function estimateAnalyticMainTrackHit({ car, track, origin, originState, ray, le
138
157
  };
139
158
  }
140
159
 
141
- function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane, sharedRayQuery }) {
160
+ function canUseDirectionalAnalyticTrackHit({ car, track, originState, ray, includePitLane }) {
161
+ if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return false;
162
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
163
+ if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return false;
164
+
165
+ const lateral = ray.x * originState.normalX + ray.y * originState.normalY;
166
+ return Math.abs(lateral) >= 0.08;
167
+ }
168
+
169
+ function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane, precision, sharedRayQuery }) {
142
170
  if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return null;
143
171
  if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
144
172
  const trackHalfWidth = track.width / 2;
@@ -154,10 +182,25 @@ function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthM
154
182
  );
155
183
  if (!boundaries.available) return null;
156
184
  if (boundaries.trackEdgeDistance == null) return null;
185
+ const kind = inside ? 'exit' : 'entry';
186
+ const hitDistance = validatedIndexedTransitionDistance({
187
+ track,
188
+ car,
189
+ origin,
190
+ ray,
191
+ progressHint: car.progress,
192
+ distance: boundaries.trackEdgeDistance,
193
+ maxDistance: metersToSimUnits(lengthMeters),
194
+ kind,
195
+ includePitLane,
196
+ precision,
197
+ originInside: inside,
198
+ });
199
+ if (hitDistance == null) return null;
157
200
  return {
158
201
  hit: true,
159
- distanceMeters: simUnitsToMeters(boundaries.trackEdgeDistance),
160
- kind: inside ? 'exit' : 'entry',
202
+ distanceMeters: simUnitsToMeters(hitDistance),
203
+ kind,
161
204
  };
162
205
  }
163
206
 
@@ -223,12 +266,13 @@ function refineTrackTransitionDistance(
223
266
  kind,
224
267
  includePitLane = true,
225
268
  refineSteps = TRACK_RAY_REFINE_STEPS,
269
+ precision = 'debug',
226
270
  ) {
227
271
  let low = lowDistance;
228
272
  let high = highDistance;
229
273
  for (let index = 0; index < refineSteps; index += 1) {
230
274
  const middle = (low + high) / 2;
231
- const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint);
275
+ const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint, precision);
232
276
  const inside = isInsideTrackBorder(state, track, includePitLane);
233
277
  if (kind === 'entry') {
234
278
  if (inside) high = middle;
@@ -242,15 +286,19 @@ function refineTrackTransitionDistance(
242
286
  return high;
243
287
  }
244
288
 
245
- function nearestRayTrackState(track, car, point, progressHint) {
289
+ function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
246
290
  const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
247
291
  return nearestTrackState(track, point, progressHint, {
248
292
  allowPitOverride: pitOverrideAllowedForCar(car),
249
- indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
293
+ indexMode: usesSampledDriverContract(car, precision) ? 'legacy' : useBatchTrainingIndexMode ? 'sample' : undefined,
250
294
  hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
251
295
  });
252
296
  }
253
297
 
298
+ function usesSampledDriverContract(car, precision) {
299
+ return precision === 'driver' && !usesMainTrackOnlyRays(car);
300
+ }
301
+
254
302
  function refineStepsForPrecision(precision) {
255
303
  return precision === 'debug' ? TRACK_RAY_REFINE_STEPS : DRIVER_RAY_REFINE_STEPS;
256
304
  }
@@ -259,3 +307,118 @@ function isInsideTrackBorder(state, track, includePitLane = true) {
259
307
  if (includePitLane && state.inPitLane) return true;
260
308
  return state.crossTrackError <= track.width / 2;
261
309
  }
310
+
311
+ function validatedIndexedTransitionDistance({
312
+ track,
313
+ car,
314
+ origin,
315
+ ray,
316
+ progressHint,
317
+ distance,
318
+ maxDistance,
319
+ kind,
320
+ includePitLane,
321
+ precision = 'driver',
322
+ originInside = kind === 'exit',
323
+ }) {
324
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
325
+ const refineSteps = refineStepsForPrecision(precision);
326
+ if (refineSteps <= 0 && usesMainTrackOnlyRays(car)) return distance;
327
+ if (refineSteps <= 0) {
328
+ return sampledIndexedTransitionDistance({
329
+ track,
330
+ car,
331
+ origin,
332
+ ray,
333
+ progressHint,
334
+ distance,
335
+ maxDistance,
336
+ kind,
337
+ includePitLane,
338
+ });
339
+ }
340
+
341
+ const windows = [step * 1.5, step * 8];
342
+
343
+ for (const searchWindow of windows) {
344
+ const low = Math.max(0, distance - searchWindow);
345
+ const high = Math.min(maxDistance, distance + searchWindow);
346
+ if (high <= low) continue;
347
+ const previousInside = validatedInsideTrackBorder(track, car, origin, ray, low, progressHint, includePitLane);
348
+ const inside = validatedInsideTrackBorder(track, car, origin, ray, high, progressHint, includePitLane);
349
+ const matched = kind === 'entry'
350
+ ? !previousInside && inside
351
+ : previousInside && !inside;
352
+ if (matched) {
353
+ if (refineSteps <= 0) return distance;
354
+ return refineValidatedTransitionDistance(
355
+ track,
356
+ car,
357
+ origin,
358
+ ray,
359
+ progressHint,
360
+ low,
361
+ high,
362
+ kind,
363
+ includePitLane,
364
+ refineSteps,
365
+ );
366
+ }
367
+ }
368
+
369
+ return null;
370
+ }
371
+
372
+ function sampledIndexedTransitionDistance({
373
+ track,
374
+ car,
375
+ origin,
376
+ ray,
377
+ progressHint,
378
+ distance,
379
+ maxDistance,
380
+ kind,
381
+ includePitLane,
382
+ }) {
383
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
384
+ const firstIndex = Math.max(1, Math.floor(Math.max(0, distance - step * 1.5) / step));
385
+ const lastIndex = Math.ceil(Math.min(maxDistance, distance + step * 8) / step);
386
+ for (let index = firstIndex; index <= lastIndex; index += 1) {
387
+ const high = Math.min(maxDistance, index * step);
388
+ const low = Math.max(0, high - step);
389
+ if (high <= low) continue;
390
+ const previousInside = validatedInsideTrackBorder(track, car, origin, ray, low, progressHint, includePitLane);
391
+ const inside = validatedInsideTrackBorder(track, car, origin, ray, high, progressHint, includePitLane);
392
+ const matched = kind === 'entry'
393
+ ? !previousInside && inside
394
+ : previousInside && !inside;
395
+ if (matched) return high;
396
+ }
397
+ return null;
398
+ }
399
+
400
+ function refineValidatedTransitionDistance(track, car, origin, ray, progressHint, lowDistance, highDistance, kind, includePitLane, refineSteps) {
401
+ let low = lowDistance;
402
+ let high = highDistance;
403
+ for (let index = 0; index < refineSteps; index += 1) {
404
+ const middle = (low + high) / 2;
405
+ const inside = validatedInsideTrackBorder(track, car, origin, ray, middle, progressHint, includePitLane);
406
+ if (kind === 'entry') {
407
+ if (inside) high = middle;
408
+ else low = middle;
409
+ } else if (inside) {
410
+ low = middle;
411
+ } else {
412
+ high = middle;
413
+ }
414
+ }
415
+ return high;
416
+ }
417
+
418
+ function validatedInsideTrackBorder(track, car, origin, ray, distance, progressHint, includePitLane) {
419
+ const state = nearestTrackState(track, pointOnRay(origin, ray, distance), progressHint, {
420
+ allowPitOverride: pitOverrideAllowedForCar(car),
421
+ indexMode: 'legacy',
422
+ });
423
+ return isInsideTrackBorder(state, track, includePitLane);
424
+ }