@inventure71/paddockjs 2.0.1 → 3.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 (80) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/INSTALL_AND_UPDATE.md +10 -1
  3. package/README.md +65 -15
  4. package/docs/architecture.md +17 -13
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/concepts.md +2 -2
  7. package/docs/custom_model_controller.md +2 -2
  8. package/docs/data_contract.md +96 -21
  9. package/docs/index.md +2 -1
  10. package/docs/learnings.md +6 -6
  11. package/docs/rules.md +7 -7
  12. package/docs/sense_contract.md +1 -1
  13. package/docs/system_specs.md +25 -18
  14. package/docs/training.md +13 -12
  15. package/package.json +1 -1
  16. package/src/api/PaddockSimulatorController.js +74 -4
  17. package/src/app/F1SimulatorApp.js +191 -28
  18. package/src/app/banners/raceDataBannerController.js +71 -36
  19. package/src/app/banners/stewardMessageController.js +5 -4
  20. package/src/app/camera/cameraConstants.js +2 -0
  21. package/src/app/camera/cameraController.js +86 -29
  22. package/src/app/camera/cameraGeometry.js +12 -1
  23. package/src/app/domBindings.js +89 -9
  24. package/src/app/layoutScheduler.js +27 -0
  25. package/src/app/layoutSupport.js +165 -0
  26. package/src/app/raceOverlayClearanceSupport.js +158 -0
  27. package/src/app/readouts/carOverviewRenderer.js +16 -8
  28. package/src/app/readouts/controlStateRenderer.js +1 -1
  29. package/src/app/readouts/raceStatusRenderer.js +30 -21
  30. package/src/app/readouts/readoutFormatters.js +2 -0
  31. package/src/app/readouts/telemetryRenderer.js +17 -8
  32. package/src/app/readouts/timingTowerRenderer.js +62 -10
  33. package/src/app/runtime/runtimeVisibility.js +66 -5
  34. package/src/config/cssValues.js +16 -0
  35. package/src/config/defaultOptions.js +51 -38
  36. package/src/config/themeOptions.js +825 -0
  37. package/src/config/timingGapMode.js +13 -0
  38. package/src/config/urlOptions.js +25 -0
  39. package/src/data/championship.js +14 -9
  40. package/src/environment/controllerLoop.js +17 -3
  41. package/src/environment/index.d.ts +1 -2
  42. package/src/environment/options.js +1 -1
  43. package/src/environment/runtime.js +3 -11
  44. package/src/environment/scenarios.js +1 -1
  45. package/src/environment/sensors/surfaceRays.js +41 -33
  46. package/src/environment/sensors/trackRays.js +44 -49
  47. package/src/index.d.ts +110 -9
  48. package/src/index.js +25 -1
  49. package/src/simulation/driver/driverController.js +1 -1
  50. package/src/simulation/driver/edgeRecovery.js +1 -1
  51. package/src/simulation/driver/racingControls.js +2 -2
  52. package/src/simulation/driver/rejoinControls.js +2 -2
  53. package/src/simulation/pit/pitKinematics.js +2 -2
  54. package/src/simulation/pit/pitRouting.js +2 -4
  55. package/src/simulation/race/raceSetup.js +2 -6
  56. package/src/simulation/race/raceStep.js +1 -1
  57. package/src/simulation/race/safetyCar.js +1 -1
  58. package/src/simulation/race/stalledDnf.js +1 -1
  59. package/src/simulation/timing/lapTelemetry.js +28 -9
  60. package/src/simulation/track/pitLaneLayout.js +7 -3
  61. package/src/simulation/track/spatialQueries.js +5 -28
  62. package/src/simulation/track/trackQueryIndex.js +180 -135
  63. package/src/simulation/track/trackStatePolicy.js +0 -1
  64. package/src/simulation/vehicle/contactResolution.js +3 -3
  65. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  66. package/src/simulation/vehicle/vehiclePhysics.js +3 -3
  67. package/src/simulation/vehicle/vehicleSnapshots.js +12 -1
  68. package/src/simulation/vehicle/vehicleState.js +1 -1
  69. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  70. package/src/styles.css +1185 -136
  71. package/src/ui/bannerTemplates.js +38 -28
  72. package/src/ui/cameraControlsTemplate.js +16 -5
  73. package/src/ui/carOverviewTemplate.js +12 -5
  74. package/src/ui/raceCanvasTemplate.js +31 -8
  75. package/src/ui/raceControlsTemplate.js +27 -10
  76. package/src/ui/shellTemplate.js +3 -1
  77. package/src/ui/telemetryDrawerTemplate.js +23 -10
  78. package/src/ui/telemetryTemplates.js +58 -32
  79. package/src/ui/templateUtils.js +48 -0
  80. package/src/ui/timingTowerTemplate.js +29 -12
@@ -0,0 +1,13 @@
1
+ export const TIMING_GAP_MODES = new Set(['interval', 'leader']);
2
+
3
+ export function normalizeTimingGapMode(value) {
4
+ return value === 'leader' ? 'leader' : 'interval';
5
+ }
6
+
7
+ export function getTimingGapModeLabel(mode) {
8
+ return normalizeTimingGapMode(mode) === 'leader' ? 'Gap' : 'Int';
9
+ }
10
+
11
+ export function getNextTimingGapMode(mode) {
12
+ return normalizeTimingGapMode(mode) === 'leader' ? 'interval' : 'leader';
13
+ }
@@ -0,0 +1,25 @@
1
+ const SAFE_ABSOLUTE_URL_PROTOCOLS = new Set(['http:', 'https:']);
2
+
3
+ export function normalizePublicUrlOption(value, fallback) {
4
+ const fallbackValue = typeof fallback === 'string' && fallback.trim() !== ''
5
+ ? fallback.trim()
6
+ : '';
7
+ if (typeof value !== 'string') return fallbackValue;
8
+
9
+ const candidate = value.trim();
10
+ if (candidate === '') return fallbackValue;
11
+ if (/[\u0000-\u001F\u007F]/.test(candidate)) return fallbackValue;
12
+ if (candidate.includes('\\')) return fallbackValue;
13
+ if (candidate.startsWith('#')) return candidate;
14
+ if (candidate.startsWith('//')) return fallbackValue;
15
+
16
+ const schemeMatch = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(candidate);
17
+ if (!schemeMatch) return candidate;
18
+
19
+ try {
20
+ const url = new URL(candidate);
21
+ return SAFE_ABSOLUTE_URL_PROTOCOLS.has(url.protocol) ? candidate : fallbackValue;
22
+ } catch {
23
+ return fallbackValue;
24
+ }
25
+ }
@@ -1,6 +1,7 @@
1
1
  import { DriverData } from './driverData.js';
2
2
  import { DEMO_PROJECT_DRIVERS } from './demoDrivers.js';
3
3
  import { VehicleData } from './vehicleData.js';
4
+ import { DEFAULT_DARK_THEME_TOKENS, normalizeCssColorToken } from '../config/themeOptions.js';
4
5
 
5
6
  export { DriverData, DRIVER_STAT_DEFINITIONS } from './driverData.js';
6
7
  export { VehicleData, VEHICLE_STAT_DEFINITIONS } from './vehicleData.js';
@@ -187,7 +188,7 @@ function normalizeTeam(team, driver, timingCode) {
187
188
  ...source,
188
189
  id,
189
190
  name,
190
- color: source.color ?? driver.color,
191
+ color: normalizeCssColorToken(source.color ?? driver.color, driver.color),
191
192
  icon: source.icon ?? normalizeTeamIcon(source.name ?? source.id ?? timingCode ?? driver.name),
192
193
  };
193
194
  }
@@ -201,22 +202,26 @@ export function buildChampionshipDriverGrid(drivers = DEMO_PROJECT_DRIVERS, entr
201
202
  const usedTimingCodes = new Set();
202
203
 
203
204
  return drivers.map((driver, index) => {
205
+ const safeDriver = {
206
+ ...driver,
207
+ color: normalizeCssColorToken(driver.color, DEFAULT_DARK_THEME_TOKENS.primary),
208
+ };
204
209
  const entry = {
205
- ...(blueprintByDriverId.get(driver.id) ?? {}),
206
- ...(entryByDriverId.get(driver.id) ?? {}),
210
+ ...(blueprintByDriverId.get(safeDriver.id) ?? {}),
211
+ ...(entryByDriverId.get(safeDriver.id) ?? {}),
207
212
  };
208
- const timingCode = pickUniqueTimingCode(entry.timingName ?? driver.timingName ?? driver.name ?? driver.code, usedTimingCodes);
209
- const driverNumber = entry.driverNumber ?? driver.driverNumber ?? index + 1;
210
- const driverArgs = buildDriverConstructorArgs({ driverId: driver.id, ...entry });
211
- const vehicleArgs = buildVehicleConstructorArgs({ driverId: driver.id, ...entry });
213
+ const timingCode = pickUniqueTimingCode(entry.timingName ?? safeDriver.timingName ?? safeDriver.name ?? safeDriver.code, usedTimingCodes);
214
+ const driverNumber = entry.driverNumber ?? safeDriver.driverNumber ?? index + 1;
215
+ const driverArgs = buildDriverConstructorArgs({ driverId: safeDriver.id, ...entry });
216
+ const vehicleArgs = buildVehicleConstructorArgs({ driverId: safeDriver.id, ...entry });
212
217
 
213
218
  return {
214
- ...driver,
219
+ ...safeDriver,
215
220
  code: timingCode,
216
221
  timingCode,
217
222
  raceName: timingCode,
218
223
  driverNumber,
219
- team: normalizeTeam(entry.team, driver, timingCode),
224
+ team: normalizeTeam(entry.team, safeDriver, timingCode),
220
225
  pace: driverArgs.pace,
221
226
  racecraft: driverArgs.racecraft,
222
227
  consistency: driverArgs.consistency,
@@ -168,8 +168,7 @@ export function createPaddockDriverControllerLoop({
168
168
 
169
169
  function stop() {
170
170
  running = false;
171
- if (scheduled && typeof scheduled.cancel === 'function') scheduled.cancel();
172
- else if (typeof scheduled === 'number' && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(scheduled);
171
+ cancelScheduled(scheduled);
173
172
  scheduled = null;
174
173
  }
175
174
 
@@ -250,9 +249,24 @@ function now() {
250
249
  : Date.now();
251
250
  }
252
251
 
252
+ function cancelScheduled(handle) {
253
+ if (handle == null) return;
254
+ if (typeof handle.cancel === 'function') {
255
+ handle.cancel();
256
+ return;
257
+ }
258
+ if (typeof handle === 'number') {
259
+ if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(handle);
260
+ if (typeof clearTimeout === 'function') clearTimeout(handle);
261
+ }
262
+ }
263
+
253
264
  function defaultScheduler() {
254
265
  if (typeof requestAnimationFrame === 'function') {
255
- return (callback) => requestAnimationFrame(callback);
266
+ return (callback) => {
267
+ const id = requestAnimationFrame(callback);
268
+ return { cancel: () => cancelAnimationFrame(id) };
269
+ };
256
270
  }
257
271
  return (callback) => {
258
272
  const id = setTimeout(callback, 0);
@@ -1,5 +1,5 @@
1
1
  export type TireCompound = 'S' | 'M' | 'H';
2
- export type PaddockPhysicsMode = 'arcade' | 'simulator';
2
+ export type PaddockPhysicsMode = 'arcade' | 'advanced';
3
3
  export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
4
4
 
5
5
  export interface PaddockProceduralTrackOptions {
@@ -478,7 +478,6 @@ export interface PaddockEnvironmentOptions {
478
478
  seed?: number;
479
479
  trackSeed?: number;
480
480
  trackGeneration?: PaddockProceduralTrackOptions;
481
- trackQueryIndex?: boolean;
482
481
  totalLaps?: number;
483
482
  frameSkip?: number;
484
483
  physicsMode?: PaddockPhysicsMode;
@@ -8,7 +8,7 @@ import { normalizeRayOptions } from './sensors.js';
8
8
 
9
9
  const DEFAULT_SEED = 1971;
10
10
  const DEFAULT_FRAME_SKIP = 1;
11
- const DEFAULT_MAX_STEPS = 10000;
11
+ const DEFAULT_MAX_STEPS = 1_000_000;
12
12
  const RESULT_STATE_OUTPUTS = new Set(['full', 'minimal', 'none']);
13
13
  const RESET_OBSERVATION_SCOPES = new Set(['all', 'reset']);
14
14
 
@@ -38,15 +38,11 @@ export function createPaddockEnvironment(options = {}) {
38
38
  }
39
39
 
40
40
  function createSimulationWithEnvironmentScenario(options) {
41
- const simulationOptions = {
42
- ...options,
43
- trackQueryIndex: shouldUseTrackQueryIndex(options),
44
- };
45
41
  runWarmupWithGuard({
46
- options: simulationOptions,
42
+ options,
47
43
  surface: 'environment',
48
44
  execute: ({ warmup }) => {
49
- const warmupSimulation = createRaceSimulation(disableWarmupOptions(simulationOptions, warmup.surface));
45
+ const warmupSimulation = createRaceSimulation(disableWarmupOptions(options, warmup.surface));
50
46
  initializeControlledPitIntentForSimulation(warmupSimulation, options);
51
47
  applyEnvironmentScenario(warmupSimulation, options);
52
48
  applyControlledRunoffResponse(warmupSimulation, options);
@@ -59,17 +55,13 @@ function createSimulationWithEnvironmentScenario(options) {
59
55
  });
60
56
  },
61
57
  });
62
- const sim = createRaceSimulation(disableWarmupOptions(simulationOptions, 'environment'));
58
+ const sim = createRaceSimulation(disableWarmupOptions(options, 'environment'));
63
59
  initializeControlledPitIntentForSimulation(sim, options);
64
60
  applyEnvironmentScenario(sim, options);
65
61
  applyControlledRunoffResponse(sim, options);
66
62
  return sim;
67
63
  }
68
64
 
69
- function shouldUseTrackQueryIndex(options) {
70
- return options.trackQueryIndex !== false;
71
- }
72
-
73
65
  export function createEnvironmentRuntime(host) {
74
66
  const episodeState = createEpisodeState();
75
67
 
@@ -233,7 +233,7 @@ function placementToCarState(track, currentCar, placement, { physicsMode = 'arca
233
233
  };
234
234
  if (speed != null) {
235
235
  partial.speed = speed;
236
- if (physicsMode === 'simulator') {
236
+ if (physicsMode === 'advanced') {
237
237
  partial.velocityX = Math.cos(heading) * speed;
238
238
  partial.velocityY = Math.sin(heading) * speed;
239
239
  } else {
@@ -47,29 +47,27 @@ export function estimateSurfaceHits(
47
47
  ? context.originState
48
48
  : nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
49
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
- }
50
+ const indexedHits = estimateIndexedSurfaceHits({
51
+ car,
52
+ track: snapshot.track,
53
+ ray,
54
+ origin,
55
+ vector,
56
+ requested,
57
+ originState,
58
+ precision,
59
+ sharedRayQuery,
60
+ });
61
+ if (indexedHits) return { ...misses, ...indexedHits, ...originHits };
62
+ const analyticHits = estimateAnalyticSurfaceHits({
63
+ car,
64
+ track: snapshot.track,
65
+ ray,
66
+ vector,
67
+ requested,
68
+ originState,
69
+ });
70
+ if (analyticHits) return { ...misses, ...analyticHits, ...originHits };
73
71
 
74
72
  const pending = new Set(requested);
75
73
  Object.keys(originHits).forEach((channel) => {
@@ -165,7 +163,6 @@ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, origi
165
163
  function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery }) {
166
164
  if (!originState || car.inPitLane || originState.inPitLane) return null;
167
165
  if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
168
- if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
169
166
 
170
167
  const trackHalfWidth = track.width / 2;
171
168
  const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
@@ -191,8 +188,16 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
191
188
  const boundaryDistance = channel === 'kerb'
192
189
  ? minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
193
190
  : boundaries.kerbOuterDistance;
194
- if (boundaryDistance == null) return null;
195
- const hitDistance = precision === 'debug' || usesMainTrackOnlyRays(car)
191
+ if (boundaryDistance == null) {
192
+ if (usesMainTrackOnlyRays(car) && channel === 'illegalSurface') {
193
+ hits[channel] = createSurfaceMiss(ray.lengthMeters);
194
+ continue;
195
+ }
196
+ if (canSampleRecoverySurface(track, originState, vector)) return null;
197
+ hits[channel] = createSurfaceMiss(ray.lengthMeters);
198
+ continue;
199
+ }
200
+ const hitDistance = usesMainTrackOnlyRays(car) || refineStepsForPrecision(precision) <= 0 || precision === 'debug'
196
201
  ? boundaryDistance
197
202
  : sampledIndexedSurfaceDistance({
198
203
  track,
@@ -341,19 +346,22 @@ function matchesSurfaceChannel(channel, state) {
341
346
  return false;
342
347
  }
343
348
 
349
+ function canSampleRecoverySurface(track, originState, vector) {
350
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
351
+ const kerbOuter = track.width / 2 + (track.kerbWidth ?? 0);
352
+ const offset = originState.signedOffset ?? 0;
353
+ if (Math.abs(offset) <= kerbOuter) return false;
354
+ const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
355
+ if (Math.abs(lateral) < 0.05) return false;
356
+ return offset > 0 ? lateral < 0 : lateral > 0;
357
+ }
358
+
344
359
  function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
345
- const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
346
360
  return nearestTrackState(track, point, progressHint, {
347
361
  allowPitOverride: pitOverrideAllowedForCar(car),
348
- indexMode: usesSampledDriverContract(car, precision) ? 'legacy' : useBatchTrainingIndexMode ? 'sample' : undefined,
349
- hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
350
362
  });
351
363
  }
352
364
 
353
- function usesSampledDriverContract(car, precision) {
354
- return precision === 'driver' && !usesMainTrackOnlyRays(car);
355
- }
356
-
357
365
  function minFinite(...values) {
358
366
  const minimum = Math.min(...values.filter((value) => Number.isFinite(value)));
359
367
  return Number.isFinite(minimum) ? minimum : null;
@@ -43,6 +43,18 @@ export function estimateTrackHit(
43
43
  ? context.originState
44
44
  : nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
45
45
  const includePitLane = Boolean(car.inPitLane || car.pitLanePart || originState.inPitLane);
46
+ const indexedHit = estimateIndexedTrackHit({
47
+ car,
48
+ track: snapshot.track,
49
+ origin,
50
+ originState,
51
+ ray,
52
+ lengthMeters,
53
+ includePitLane,
54
+ precision,
55
+ sharedRayQuery,
56
+ });
57
+ if (indexedHit) return indexedHit;
46
58
  const directionalAnalyticHit = canUseDirectionalAnalyticTrackHit({ car, track: snapshot.track, originState, ray, includePitLane })
47
59
  ? estimateAnalyticMainTrackHit({
48
60
  car,
@@ -55,20 +67,6 @@ export function estimateTrackHit(
55
67
  })
56
68
  : null;
57
69
  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
- }
72
70
  const analyticHit = estimateAnalyticMainTrackHit({
73
71
  car,
74
72
  track: snapshot.track,
@@ -168,7 +166,6 @@ function canUseDirectionalAnalyticTrackHit({ car, track, originState, ray, inclu
168
166
 
169
167
  function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane, precision, sharedRayQuery }) {
170
168
  if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return null;
171
- if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
172
169
  const trackHalfWidth = track.width / 2;
173
170
  const inside = Math.abs(originState.signedOffset ?? 0) <= trackHalfWidth;
174
171
  const boundaries = findIndexedTrackBandBoundaries(
@@ -181,21 +178,28 @@ function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthM
181
178
  sharedRayQuery,
182
179
  );
183
180
  if (!boundaries.available) return null;
184
- if (boundaries.trackEdgeDistance == null) return null;
181
+ if (boundaries.trackEdgeDistance == null) {
182
+ if (usesMainTrackOnlyRays(car)) return createTrackMiss(lengthMeters);
183
+ return canSampleRecoveryTransition(track, originState, ray)
184
+ ? null
185
+ : createTrackMiss(lengthMeters);
186
+ }
185
187
  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
- });
188
+ const hitDistance = usesMainTrackOnlyRays(car)
189
+ ? boundaries.trackEdgeDistance
190
+ : validatedIndexedTransitionDistance({
191
+ track,
192
+ car,
193
+ origin,
194
+ ray,
195
+ progressHint: car.progress,
196
+ distance: boundaries.trackEdgeDistance,
197
+ maxDistance: metersToSimUnits(lengthMeters),
198
+ kind,
199
+ includePitLane,
200
+ precision,
201
+ originInside: inside,
202
+ });
199
203
  if (hitDistance == null) return null;
200
204
  return {
201
205
  hit: true,
@@ -287,18 +291,11 @@ function refineTrackTransitionDistance(
287
291
  }
288
292
 
289
293
  function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
290
- const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
291
294
  return nearestTrackState(track, point, progressHint, {
292
295
  allowPitOverride: pitOverrideAllowedForCar(car),
293
- indexMode: usesSampledDriverContract(car, precision) ? 'legacy' : useBatchTrainingIndexMode ? 'sample' : undefined,
294
- hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
295
296
  });
296
297
  }
297
298
 
298
- function usesSampledDriverContract(car, precision) {
299
- return precision === 'driver' && !usesMainTrackOnlyRays(car);
300
- }
301
-
302
299
  function refineStepsForPrecision(precision) {
303
300
  return precision === 'debug' ? TRACK_RAY_REFINE_STEPS : DRIVER_RAY_REFINE_STEPS;
304
301
  }
@@ -308,6 +305,16 @@ function isInsideTrackBorder(state, track, includePitLane = true) {
308
305
  return state.crossTrackError <= track.width / 2;
309
306
  }
310
307
 
308
+ function canSampleRecoveryTransition(track, originState, ray) {
309
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
310
+ const trackHalfWidth = track.width / 2;
311
+ const offset = originState.signedOffset ?? 0;
312
+ if (Math.abs(offset) <= trackHalfWidth) return false;
313
+ const lateral = ray.x * originState.normalX + ray.y * originState.normalY;
314
+ if (Math.abs(lateral) < 0.05) return false;
315
+ return offset > 0 ? lateral < 0 : lateral > 0;
316
+ }
317
+
311
318
  function validatedIndexedTransitionDistance({
312
319
  track,
313
320
  car,
@@ -323,19 +330,8 @@ function validatedIndexedTransitionDistance({
323
330
  }) {
324
331
  const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
325
332
  const refineSteps = refineStepsForPrecision(precision);
326
- if (refineSteps <= 0 && usesMainTrackOnlyRays(car)) return distance;
327
333
  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
- });
334
+ return distance;
339
335
  }
340
336
 
341
337
  const windows = [step * 1.5, step * 8];
@@ -418,7 +414,6 @@ function refineValidatedTransitionDistance(track, car, origin, ray, progressHint
418
414
  function validatedInsideTrackBorder(track, car, origin, ray, distance, progressHint, includePitLane) {
419
415
  const state = nearestTrackState(track, pointOnRay(origin, ray, distance), progressHint, {
420
416
  allowPitOverride: pitOverrideAllowedForCar(car),
421
- indexMode: 'legacy',
422
417
  });
423
418
  return isInsideTrackBorder(state, track, includePitLane);
424
419
  }