@inventure71/paddockjs 2.0.0 → 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 (110) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/INSTALL_AND_UPDATE.md +10 -1
  3. package/README.md +92 -17
  4. package/docs/architecture.md +26 -16
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/concepts.md +7 -5
  7. package/docs/custom_model_controller.md +2 -2
  8. package/docs/data_contract.md +121 -33
  9. package/docs/data_model.md +1 -1
  10. package/docs/index.md +10 -0
  11. package/docs/learnings.md +6 -6
  12. package/docs/rules.md +39 -9
  13. package/docs/sense_contract.md +59 -0
  14. package/docs/system_specs.md +76 -20
  15. package/docs/training.md +27 -20
  16. package/package.json +1 -1
  17. package/src/api/PaddockSimulatorController.js +75 -36
  18. package/src/app/BrowserExpertAdapter.js +9 -300
  19. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  20. package/src/app/F1SimulatorApp.js +190 -68
  21. package/src/app/banners/raceDataBannerController.js +71 -36
  22. package/src/app/banners/stewardMessageController.js +5 -4
  23. package/src/app/camera/cameraConstants.js +2 -0
  24. package/src/app/camera/cameraController.js +86 -29
  25. package/src/app/camera/cameraGeometry.js +12 -1
  26. package/src/app/domBindings.js +89 -9
  27. package/src/app/layoutScheduler.js +27 -0
  28. package/src/app/layoutSupport.js +165 -0
  29. package/src/app/raceOverlayClearanceSupport.js +158 -0
  30. package/src/app/readouts/carOverviewRenderer.js +16 -8
  31. package/src/app/readouts/controlStateRenderer.js +1 -1
  32. package/src/app/readouts/raceStatusRenderer.js +30 -21
  33. package/src/app/readouts/readoutFormatters.js +2 -0
  34. package/src/app/readouts/telemetryRenderer.js +17 -8
  35. package/src/app/readouts/timingTowerRenderer.js +62 -10
  36. package/src/app/runtime/runtimeVisibility.js +66 -5
  37. package/src/config/cssValues.js +16 -0
  38. package/src/config/defaultOptions.js +51 -38
  39. package/src/config/restartOptions.js +34 -0
  40. package/src/config/themeOptions.js +825 -0
  41. package/src/config/timingGapMode.js +13 -0
  42. package/src/config/urlOptions.js +25 -0
  43. package/src/data/championship.js +14 -9
  44. package/src/environment/controllerLoop.js +34 -5
  45. package/src/environment/episode.js +14 -2
  46. package/src/environment/index.d.ts +17 -4
  47. package/src/environment/observationVector.js +200 -0
  48. package/src/environment/observations.js +58 -285
  49. package/src/environment/options.js +1 -1
  50. package/src/environment/recorder.js +13 -6
  51. package/src/environment/rewards.js +13 -2
  52. package/src/environment/runtime.js +65 -25
  53. package/src/environment/scenarios.js +22 -3
  54. package/src/environment/sensors/index.js +3 -3
  55. package/src/environment/sensors/opponentRadar.js +13 -3
  56. package/src/environment/sensors/sensorTargets.js +8 -2
  57. package/src/environment/sensors/surfaceRays.js +84 -12
  58. package/src/environment/sensors/trackRays.js +173 -15
  59. package/src/environment/specs.js +78 -40
  60. package/src/index.d.ts +112 -9
  61. package/src/index.js +26 -33
  62. package/src/simulation/driver/driverController.js +1 -1
  63. package/src/simulation/driver/edgeRecovery.js +1 -1
  64. package/src/simulation/driver/racingControls.js +2 -2
  65. package/src/simulation/driver/rejoinControls.js +2 -2
  66. package/src/simulation/pit/pitFlow.js +5 -0
  67. package/src/simulation/pit/pitKinematics.js +24 -0
  68. package/src/simulation/pit/pitQueue.js +5 -0
  69. package/src/simulation/pit/pitRouteMovement.js +7 -0
  70. package/src/simulation/pit/pitRouting.js +2 -4
  71. package/src/simulation/pit/pitService.js +2 -0
  72. package/src/simulation/pit/pitTireService.js +2 -0
  73. package/src/simulation/race/raceLifecycle.js +2 -6
  74. package/src/simulation/race/raceProgress.js +6 -1
  75. package/src/simulation/race/raceSetup.js +2 -6
  76. package/src/simulation/race/raceStep.js +2 -2
  77. package/src/simulation/race/redFlag.js +3 -3
  78. package/src/simulation/race/safetyCar.js +10 -4
  79. package/src/simulation/race/stalledDnf.js +8 -17
  80. package/src/simulation/raceSimulation.js +1 -0
  81. package/src/simulation/rules/rulesReview.js +3 -0
  82. package/src/simulation/snapshots/raceSnapshots.js +1 -0
  83. package/src/simulation/timing/lapTelemetry.js +28 -9
  84. package/src/simulation/track/pitLaneLayout.js +7 -3
  85. package/src/simulation/track/spatialQueries.js +5 -28
  86. package/src/simulation/track/trackQueryGrid.js +94 -0
  87. package/src/simulation/track/trackQueryIndex.js +193 -434
  88. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  89. package/src/simulation/track/trackQueryScratch.js +46 -0
  90. package/src/simulation/track/trackQueryStats.js +28 -0
  91. package/src/simulation/track/trackStatePolicy.js +0 -1
  92. package/src/simulation/vehicle/contactResolution.js +67 -2
  93. package/src/simulation/vehicle/runoffResponse.js +11 -12
  94. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  95. package/src/simulation/vehicle/vehiclePhysics.js +5 -4
  96. package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
  97. package/src/simulation/vehicle/vehicleState.js +36 -2
  98. package/src/simulation/vehicle/wheelSurface.js +3 -1
  99. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  100. package/src/styles.css +1185 -136
  101. package/src/ui/bannerTemplates.js +38 -28
  102. package/src/ui/cameraControlsTemplate.js +16 -5
  103. package/src/ui/carOverviewTemplate.js +12 -5
  104. package/src/ui/raceCanvasTemplate.js +31 -8
  105. package/src/ui/raceControlsTemplate.js +27 -10
  106. package/src/ui/shellTemplate.js +3 -1
  107. package/src/ui/telemetryDrawerTemplate.js +23 -10
  108. package/src/ui/telemetryTemplates.js +58 -32
  109. package/src/ui/templateUtils.js +48 -0
  110. package/src/ui/timingTowerTemplate.js +29 -12
@@ -182,7 +182,9 @@ export function applyEnvironmentPlacements(sim, track, carsById, placements = {}
182
182
  Object.entries(placements).forEach(([driverId, placement]) => {
183
183
  const currentCar = carsById.get(driverId);
184
184
  if (!currentCar) return;
185
- states[driverId] = placementToCarState(track, currentCar, placement);
185
+ states[driverId] = placementToCarState(track, currentCar, placement, {
186
+ physicsMode: sim.physicsMode,
187
+ });
186
188
  });
187
189
  if (typeof sim.setCarStates === 'function') {
188
190
  sim.setCarStates(states);
@@ -193,13 +195,14 @@ export function applyEnvironmentPlacements(sim, track, carsById, placements = {}
193
195
  });
194
196
  }
195
197
 
196
- function placementToCarState(track, currentCar, placement) {
198
+ function placementToCarState(track, currentCar, placement, { physicsMode = 'arcade' } = {}) {
197
199
  const currentDistanceMeters = currentCar.distanceMeters ?? simUnitsToMeters(currentCar.raceDistance ?? currentCar.progress ?? 0);
198
200
  const raceDistance = metersToSimUnits(Math.max(0, placement.distanceMeters ?? currentDistanceMeters));
199
201
  const offset = metersToSimUnits(placement.offsetMeters ?? 0);
200
202
  const base = pointAt(track, raceDistance);
201
203
  const position = offsetTrackPoint(base, offset);
202
204
  const heading = normalizeAngle(base.heading + (placement.headingErrorRadians ?? 0));
205
+ const speed = placement.speedKph == null ? null : kphToSimSpeed(placement.speedKph);
203
206
  const partial = {
204
207
  x: position.x,
205
208
  y: position.y,
@@ -217,11 +220,27 @@ function placementToCarState(track, currentCar, placement) {
217
220
  canAttack: true,
218
221
  throttle: 0,
219
222
  brake: 0,
223
+ longitudinalAcceleration: 0,
224
+ lateralAcceleration: 0,
225
+ lateralG: 0,
226
+ longitudinalG: 0,
227
+ gripUsage: 0,
228
+ slipAngleRadians: 0,
229
+ tractionLimited: false,
220
230
  steeringAngle: 0,
221
231
  yawRate: 0,
222
232
  stabilityState: 'stable',
223
233
  };
224
- if (placement.speedKph != null) partial.speed = kphToSimSpeed(placement.speedKph);
234
+ if (speed != null) {
235
+ partial.speed = speed;
236
+ if (physicsMode === 'advanced') {
237
+ partial.velocityX = Math.cos(heading) * speed;
238
+ partial.velocityY = Math.sin(heading) * speed;
239
+ } else {
240
+ partial.velocityX = null;
241
+ partial.velocityY = null;
242
+ }
243
+ }
225
244
  return partial;
226
245
  }
227
246
 
@@ -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,7 +43,10 @@ export function estimateSurfaceHits(
43
43
  return misses;
44
44
  }
45
45
 
46
- const originState = context?.originState ?? nearestRayTrackState(snapshot.track, car, origin, car.progress);
46
+ const originState = context?.precision === precision
47
+ ? context.originState
48
+ : nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
49
+ const originHits = surfaceHitsAtOrigin(requested, originState);
47
50
  const indexedHits = estimateIndexedSurfaceHits({
48
51
  car,
49
52
  track: snapshot.track,
@@ -52,9 +55,10 @@ export function estimateSurfaceHits(
52
55
  vector,
53
56
  requested,
54
57
  originState,
58
+ precision,
55
59
  sharedRayQuery,
56
60
  });
57
- if (indexedHits) return { ...misses, ...indexedHits };
61
+ if (indexedHits) return { ...misses, ...indexedHits, ...originHits };
58
62
  const analyticHits = estimateAnalyticSurfaceHits({
59
63
  car,
60
64
  track: snapshot.track,
@@ -63,16 +67,21 @@ export function estimateSurfaceHits(
63
67
  requested,
64
68
  originState,
65
69
  });
66
- if (analyticHits) return { ...misses, ...analyticHits };
70
+ if (analyticHits) return { ...misses, ...analyticHits, ...originHits };
67
71
 
68
72
  const pending = new Set(requested);
73
+ Object.keys(originHits).forEach((channel) => {
74
+ misses[channel] = originHits[channel];
75
+ pending.delete(channel);
76
+ });
77
+ if (pending.size === 0) return misses;
69
78
  const maxDistance = metersToSimUnits(ray.lengthMeters);
70
79
  const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
71
80
  let previousDistance = 0;
72
81
  let previousState = null;
73
82
 
74
83
  for (let distance = 0; distance <= maxDistance; distance += step) {
75
- const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress);
84
+ const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress, precision);
76
85
  for (const channel of [...pending]) {
77
86
  if (!matchesSurfaceChannel(channel, state)) continue;
78
87
  const hitDistance = previousState
@@ -93,6 +102,19 @@ export function estimateSurfaceHits(
93
102
  return misses;
94
103
  }
95
104
 
105
+ function surfaceHitsAtOrigin(requested, originState) {
106
+ const hits = {};
107
+ requested.forEach((channel) => {
108
+ if (!matchesSurfaceChannel(channel, originState)) return;
109
+ hits[channel] = {
110
+ hit: true,
111
+ distanceMeters: 0,
112
+ surface: originState.surface ?? null,
113
+ };
114
+ });
115
+ return hits;
116
+ }
117
+
96
118
  function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState }) {
97
119
  if (!originState || car.inPitLane || originState.inPitLane) return null;
98
120
  if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
@@ -138,10 +160,9 @@ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, origi
138
160
  return hits;
139
161
  }
140
162
 
141
- function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, sharedRayQuery }) {
163
+ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery }) {
142
164
  if (!originState || car.inPitLane || originState.inPitLane) return null;
143
165
  if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
144
- if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
145
166
 
146
167
  const trackHalfWidth = track.width / 2;
147
168
  const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
@@ -167,10 +188,32 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
167
188
  const boundaryDistance = channel === 'kerb'
168
189
  ? minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
169
190
  : boundaries.kerbOuterDistance;
170
- if (boundaryDistance == null) return null;
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'
201
+ ? boundaryDistance
202
+ : sampledIndexedSurfaceDistance({
203
+ track,
204
+ car,
205
+ origin,
206
+ vector,
207
+ progressHint: car.progress,
208
+ distance: boundaryDistance,
209
+ maxDistance: metersToSimUnits(ray.lengthMeters),
210
+ channel,
211
+ precision,
212
+ });
213
+ if (hitDistance == null) return null;
171
214
  hits[channel] = {
172
215
  hit: true,
173
- distanceMeters: simUnitsToMeters(boundaryDistance),
216
+ distanceMeters: simUnitsToMeters(hitDistance),
174
217
  surface: channel === 'kerb'
175
218
  ? 'kerb'
176
219
  : surfaceBeyondKerb(track, originState.signedOffset ?? 0, lateral, metersToSimUnits(ray.lengthMeters)),
@@ -180,6 +223,28 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
180
223
  return hits;
181
224
  }
182
225
 
226
+ function sampledIndexedSurfaceDistance({
227
+ track,
228
+ car,
229
+ origin,
230
+ vector,
231
+ progressHint,
232
+ distance,
233
+ maxDistance,
234
+ channel,
235
+ precision,
236
+ }) {
237
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
238
+ const firstIndex = Math.max(0, Math.floor(Math.max(0, distance - step * 1.5) / step));
239
+ const lastIndex = Math.ceil(Math.min(maxDistance, distance + step * 8) / step);
240
+ for (let index = firstIndex; index <= lastIndex; index += 1) {
241
+ const sampleDistance = Math.min(maxDistance, index * step);
242
+ const state = nearestRayTrackState(track, car, pointOnRay(origin, vector, sampleDistance), progressHint, precision);
243
+ if (matchesSurfaceChannel(channel, state)) return sampleDistance;
244
+ }
245
+ return null;
246
+ }
247
+
183
248
  function usesMainTrackOnlyRays(car) {
184
249
  return car?.interaction?.profile === 'batch-training';
185
250
  }
@@ -281,12 +346,19 @@ function matchesSurfaceChannel(channel, state) {
281
346
  return false;
282
347
  }
283
348
 
284
- function nearestRayTrackState(track, car, point, progressHint) {
285
- const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
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
+
359
+ function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
286
360
  return nearestTrackState(track, point, progressHint, {
287
361
  allowPitOverride: pitOverrideAllowedForCar(car),
288
- indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
289
- hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
290
362
  });
291
363
  }
292
364
 
@@ -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,7 +39,9 @@ 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
46
  const indexedHit = estimateIndexedTrackHit({
44
47
  car,
@@ -48,9 +51,22 @@ export function estimateTrackHit(
48
51
  ray,
49
52
  lengthMeters,
50
53
  includePitLane,
54
+ precision,
51
55
  sharedRayQuery,
52
56
  });
53
57
  if (indexedHit) return indexedHit;
58
+ const directionalAnalyticHit = canUseDirectionalAnalyticTrackHit({ car, track: snapshot.track, originState, ray, includePitLane })
59
+ ? estimateAnalyticMainTrackHit({
60
+ car,
61
+ track: snapshot.track,
62
+ origin,
63
+ originState,
64
+ ray,
65
+ lengthMeters,
66
+ includePitLane,
67
+ })
68
+ : null;
69
+ if (directionalAnalyticHit) return directionalAnalyticHit;
54
70
  const analyticHit = estimateAnalyticMainTrackHit({
55
71
  car,
56
72
  track: snapshot.track,
@@ -65,7 +81,7 @@ export function estimateTrackHit(
65
81
  let previousInside = null;
66
82
 
67
83
  for (let distance = 0; distance <= maxDistance; distance += step) {
68
- const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, ray, distance), car.progress);
84
+ const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, ray, distance), car.progress, precision);
69
85
  const inside = isInsideTrackBorder(state, snapshot.track, includePitLane);
70
86
  if (previousInside == null) {
71
87
  previousInside = inside;
@@ -86,6 +102,7 @@ export function estimateTrackHit(
86
102
  kind,
87
103
  includePitLane,
88
104
  refineStepsForPrecision(precision),
105
+ precision,
89
106
  );
90
107
  return {
91
108
  hit: true,
@@ -138,9 +155,17 @@ function estimateAnalyticMainTrackHit({ car, track, origin, originState, ray, le
138
155
  };
139
156
  }
140
157
 
141
- function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane, sharedRayQuery }) {
158
+ function canUseDirectionalAnalyticTrackHit({ car, track, originState, ray, includePitLane }) {
159
+ if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return false;
160
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
161
+ if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return false;
162
+
163
+ const lateral = ray.x * originState.normalX + ray.y * originState.normalY;
164
+ return Math.abs(lateral) >= 0.08;
165
+ }
166
+
167
+ function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane, precision, sharedRayQuery }) {
142
168
  if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return null;
143
- if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
144
169
  const trackHalfWidth = track.width / 2;
145
170
  const inside = Math.abs(originState.signedOffset ?? 0) <= trackHalfWidth;
146
171
  const boundaries = findIndexedTrackBandBoundaries(
@@ -153,11 +178,33 @@ function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthM
153
178
  sharedRayQuery,
154
179
  );
155
180
  if (!boundaries.available) return null;
156
- 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
+ }
187
+ const kind = inside ? 'exit' : 'entry';
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
+ });
203
+ if (hitDistance == null) return null;
157
204
  return {
158
205
  hit: true,
159
- distanceMeters: simUnitsToMeters(boundaries.trackEdgeDistance),
160
- kind: inside ? 'exit' : 'entry',
206
+ distanceMeters: simUnitsToMeters(hitDistance),
207
+ kind,
161
208
  };
162
209
  }
163
210
 
@@ -223,12 +270,13 @@ function refineTrackTransitionDistance(
223
270
  kind,
224
271
  includePitLane = true,
225
272
  refineSteps = TRACK_RAY_REFINE_STEPS,
273
+ precision = 'debug',
226
274
  ) {
227
275
  let low = lowDistance;
228
276
  let high = highDistance;
229
277
  for (let index = 0; index < refineSteps; index += 1) {
230
278
  const middle = (low + high) / 2;
231
- const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint);
279
+ const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint, precision);
232
280
  const inside = isInsideTrackBorder(state, track, includePitLane);
233
281
  if (kind === 'entry') {
234
282
  if (inside) high = middle;
@@ -242,12 +290,9 @@ function refineTrackTransitionDistance(
242
290
  return high;
243
291
  }
244
292
 
245
- function nearestRayTrackState(track, car, point, progressHint) {
246
- const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
293
+ function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
247
294
  return nearestTrackState(track, point, progressHint, {
248
295
  allowPitOverride: pitOverrideAllowedForCar(car),
249
- indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
250
- hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
251
296
  });
252
297
  }
253
298
 
@@ -259,3 +304,116 @@ function isInsideTrackBorder(state, track, includePitLane = true) {
259
304
  if (includePitLane && state.inPitLane) return true;
260
305
  return state.crossTrackError <= track.width / 2;
261
306
  }
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
+
318
+ function validatedIndexedTransitionDistance({
319
+ track,
320
+ car,
321
+ origin,
322
+ ray,
323
+ progressHint,
324
+ distance,
325
+ maxDistance,
326
+ kind,
327
+ includePitLane,
328
+ precision = 'driver',
329
+ originInside = kind === 'exit',
330
+ }) {
331
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
332
+ const refineSteps = refineStepsForPrecision(precision);
333
+ if (refineSteps <= 0) {
334
+ return distance;
335
+ }
336
+
337
+ const windows = [step * 1.5, step * 8];
338
+
339
+ for (const searchWindow of windows) {
340
+ const low = Math.max(0, distance - searchWindow);
341
+ const high = Math.min(maxDistance, distance + searchWindow);
342
+ if (high <= low) continue;
343
+ const previousInside = validatedInsideTrackBorder(track, car, origin, ray, low, progressHint, includePitLane);
344
+ const inside = validatedInsideTrackBorder(track, car, origin, ray, high, progressHint, includePitLane);
345
+ const matched = kind === 'entry'
346
+ ? !previousInside && inside
347
+ : previousInside && !inside;
348
+ if (matched) {
349
+ if (refineSteps <= 0) return distance;
350
+ return refineValidatedTransitionDistance(
351
+ track,
352
+ car,
353
+ origin,
354
+ ray,
355
+ progressHint,
356
+ low,
357
+ high,
358
+ kind,
359
+ includePitLane,
360
+ refineSteps,
361
+ );
362
+ }
363
+ }
364
+
365
+ return null;
366
+ }
367
+
368
+ function sampledIndexedTransitionDistance({
369
+ track,
370
+ car,
371
+ origin,
372
+ ray,
373
+ progressHint,
374
+ distance,
375
+ maxDistance,
376
+ kind,
377
+ includePitLane,
378
+ }) {
379
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
380
+ const firstIndex = Math.max(1, Math.floor(Math.max(0, distance - step * 1.5) / step));
381
+ const lastIndex = Math.ceil(Math.min(maxDistance, distance + step * 8) / step);
382
+ for (let index = firstIndex; index <= lastIndex; index += 1) {
383
+ const high = Math.min(maxDistance, index * step);
384
+ const low = Math.max(0, high - step);
385
+ if (high <= low) continue;
386
+ const previousInside = validatedInsideTrackBorder(track, car, origin, ray, low, progressHint, includePitLane);
387
+ const inside = validatedInsideTrackBorder(track, car, origin, ray, high, progressHint, includePitLane);
388
+ const matched = kind === 'entry'
389
+ ? !previousInside && inside
390
+ : previousInside && !inside;
391
+ if (matched) return high;
392
+ }
393
+ return null;
394
+ }
395
+
396
+ function refineValidatedTransitionDistance(track, car, origin, ray, progressHint, lowDistance, highDistance, kind, includePitLane, refineSteps) {
397
+ let low = lowDistance;
398
+ let high = highDistance;
399
+ for (let index = 0; index < refineSteps; index += 1) {
400
+ const middle = (low + high) / 2;
401
+ const inside = validatedInsideTrackBorder(track, car, origin, ray, middle, progressHint, includePitLane);
402
+ if (kind === 'entry') {
403
+ if (inside) high = middle;
404
+ else low = middle;
405
+ } else if (inside) {
406
+ low = middle;
407
+ } else {
408
+ high = middle;
409
+ }
410
+ }
411
+ return high;
412
+ }
413
+
414
+ function validatedInsideTrackBorder(track, car, origin, ray, distance, progressHint, includePitLane) {
415
+ const state = nearestTrackState(track, pointOnRay(origin, ray, distance), progressHint, {
416
+ allowPitOverride: pitOverrideAllowedForCar(car),
417
+ });
418
+ return isInsideTrackBorder(state, track, includePitLane);
419
+ }