@inventure71/paddockjs 1.0.0 → 2.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 (190) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +194 -12
  4. package/docs/architecture.md +119 -44
  5. package/docs/concepts.md +4 -2
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +299 -17
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +22 -0
  10. package/docs/rules.md +37 -10
  11. package/docs/system_specs.md +23 -10
  12. package/docs/training.md +272 -30
  13. package/examples/python/README.md +62 -0
  14. package/examples/python/base_policy_server.py +191 -0
  15. package/examples/python/requirements.txt +2 -0
  16. package/package.json +12 -4
  17. package/src/api/PaddockSimulatorController.js +4 -0
  18. package/src/app/BrowserExpertAdapter.js +367 -9
  19. package/src/app/F1SimulatorApp.js +305 -1603
  20. package/src/app/banners/bannerFormatters.js +55 -0
  21. package/src/app/banners/raceDataBannerController.js +141 -0
  22. package/src/app/banners/stewardMessageController.js +99 -0
  23. package/src/app/camera/cameraConstants.js +21 -0
  24. package/src/app/camera/cameraController.js +265 -0
  25. package/src/app/camera/cameraGeometry.js +176 -0
  26. package/src/app/domBindings.js +4 -0
  27. package/src/app/readouts/carOverviewRenderer.js +109 -0
  28. package/src/app/readouts/controlStateRenderer.js +37 -0
  29. package/src/app/readouts/raceStatusRenderer.js +133 -0
  30. package/src/app/readouts/readoutFormatters.js +58 -0
  31. package/src/app/readouts/telemetryRenderer.js +113 -0
  32. package/src/app/readouts/timingTowerRenderer.js +177 -0
  33. package/src/app/rendering/appAssets.js +38 -0
  34. package/src/app/rendering/carRenderer.js +286 -0
  35. package/src/app/rendering/displayUtils.js +22 -0
  36. package/src/app/rendering/drsTrailRenderer.js +90 -0
  37. package/src/app/rendering/expertSensorRenderer.js +126 -0
  38. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  39. package/src/app/rendering/replayGhostRenderer.js +124 -0
  40. package/src/app/rendering/trackRenderer.js +25 -0
  41. package/src/app/runtime/frameLoop.js +66 -0
  42. package/src/app/runtime/runtimeTiming.js +34 -0
  43. package/src/app/runtime/runtimeVisibility.js +45 -0
  44. package/src/config/defaultOptions.js +20 -0
  45. package/src/environment/actions.js +13 -5
  46. package/src/environment/controllerLoop.js +246 -0
  47. package/src/environment/episode.js +86 -2
  48. package/src/environment/evaluation.js +54 -32
  49. package/src/environment/index.d.ts +361 -9
  50. package/src/environment/index.js +1 -0
  51. package/src/environment/metrics.js +159 -0
  52. package/src/environment/observationOptions.js +22 -1
  53. package/src/environment/observations.js +325 -40
  54. package/src/environment/options.js +37 -5
  55. package/src/environment/runtime.js +222 -21
  56. package/src/environment/scenarios.js +37 -8
  57. package/src/environment/sensors/bodySenses.js +23 -0
  58. package/src/environment/sensors/boundarySenses.js +17 -0
  59. package/src/environment/sensors/carRays.js +75 -0
  60. package/src/environment/sensors/contactSenses.js +32 -0
  61. package/src/environment/sensors/index.js +157 -0
  62. package/src/environment/sensors/indexedRayBands.js +103 -0
  63. package/src/environment/sensors/nearbyCars.js +52 -0
  64. package/src/environment/sensors/opponentRadar.js +65 -0
  65. package/src/environment/sensors/rayConfig.js +110 -0
  66. package/src/environment/sensors/rayDefaults.js +6 -0
  67. package/src/environment/sensors/rayGeometry.js +60 -0
  68. package/src/environment/sensors/rayGuards.js +19 -0
  69. package/src/environment/sensors/sensorTargets.js +60 -0
  70. package/src/environment/sensors/surfaceRays.js +296 -0
  71. package/src/environment/sensors/trackRays.js +261 -0
  72. package/src/environment/sensors.js +1 -367
  73. package/src/environment/specs.js +100 -5
  74. package/src/environment/workerProtocol.js +13 -1
  75. package/src/index.d.ts +169 -6
  76. package/src/index.js +5 -0
  77. package/src/rendering/proceduralTrackAsset.js +20 -823
  78. package/src/rendering/renderSnapshot.js +38 -12
  79. package/src/rendering/track/finishLineRenderer.js +34 -0
  80. package/src/rendering/track/gridRenderer.js +47 -0
  81. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  82. package/src/rendering/track/pitLaneRenderer.js +248 -0
  83. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  84. package/src/rendering/track/trackRenderConstants.js +38 -0
  85. package/src/rendering/track/trackRenderGeometry.js +94 -0
  86. package/src/simulation/collisionGeometry.js +1 -1
  87. package/src/simulation/driver/driverControlConstants.js +28 -0
  88. package/src/simulation/driver/driverController.js +47 -0
  89. package/src/simulation/driver/driverInput.js +28 -0
  90. package/src/simulation/driver/driverMath.js +6 -0
  91. package/src/simulation/driver/driverPersonality.js +25 -0
  92. package/src/simulation/driver/edgeRecovery.js +48 -0
  93. package/src/simulation/driver/racingControls.js +399 -0
  94. package/src/simulation/driver/racingLinePlan.js +166 -0
  95. package/src/simulation/driver/recoveryDynamics.js +38 -0
  96. package/src/simulation/driver/rejoinControls.js +213 -0
  97. package/src/simulation/driver/safetyCarControls.js +35 -0
  98. package/src/simulation/driver/trafficScan.js +51 -0
  99. package/src/simulation/driverController.js +4 -619
  100. package/src/simulation/participants/participantInteractions.js +120 -0
  101. package/src/simulation/pit/pitFlow.js +234 -0
  102. package/src/simulation/pit/pitIntent.js +92 -0
  103. package/src/simulation/pit/pitOccupancy.js +47 -0
  104. package/src/simulation/pit/pitPenaltyService.js +71 -0
  105. package/src/simulation/pit/pitQueue.js +55 -0
  106. package/src/simulation/pit/pitRouteMovement.js +160 -0
  107. package/src/simulation/pit/pitRouting.js +319 -0
  108. package/src/simulation/pit/pitService.js +41 -0
  109. package/src/simulation/pit/pitServiceConstants.js +21 -0
  110. package/src/simulation/pit/pitServiceProfile.js +45 -0
  111. package/src/simulation/pit/pitSnapshots.js +61 -0
  112. package/src/simulation/pit/pitState.js +113 -0
  113. package/src/simulation/pit/pitTireService.js +92 -0
  114. package/src/simulation/race/classification.js +104 -0
  115. package/src/simulation/race/gridPenalties.js +37 -0
  116. package/src/simulation/race/raceControlState.js +38 -0
  117. package/src/simulation/race/raceDistance.js +32 -0
  118. package/src/simulation/race/raceFinish.js +65 -0
  119. package/src/simulation/race/raceLifecycle.js +71 -0
  120. package/src/simulation/race/raceOrder.js +113 -0
  121. package/src/simulation/race/raceProgress.js +124 -0
  122. package/src/simulation/race/raceSetup.js +93 -0
  123. package/src/simulation/race/raceStep.js +80 -0
  124. package/src/simulation/race/redFlag.js +15 -0
  125. package/src/simulation/race/retirements.js +44 -0
  126. package/src/simulation/race/safetyCar.js +86 -0
  127. package/src/simulation/race/stalledDnf.js +86 -0
  128. package/src/simulation/raceSimulation.js +230 -2817
  129. package/src/simulation/replay/replayGhosts.js +128 -0
  130. package/src/simulation/rules/moduleConfig.js +72 -0
  131. package/src/simulation/rules/penaltyConfig.js +99 -0
  132. package/src/simulation/rules/penaltyEvents.js +18 -0
  133. package/src/simulation/rules/penaltyStats.js +26 -0
  134. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  135. package/src/simulation/rules/ruleDefaults.js +85 -0
  136. package/src/simulation/rules/rulesReview.js +141 -0
  137. package/src/simulation/rules/rulesetPresets.js +47 -0
  138. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  139. package/src/simulation/rulesConfig.js +5 -323
  140. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  141. package/src/simulation/timing/drsTiming.js +87 -0
  142. package/src/simulation/timing/gapEstimation.js +38 -0
  143. package/src/simulation/timing/lapTelemetry.js +182 -0
  144. package/src/simulation/timing/raceTiming.js +15 -0
  145. package/src/simulation/timing/sectorPerformance.js +60 -0
  146. package/src/simulation/timing/timingConstants.js +5 -0
  147. package/src/simulation/timing/timingHistory.js +46 -0
  148. package/src/simulation/timing/timingLines.js +79 -0
  149. package/src/simulation/track/centerlineSpline.js +59 -0
  150. package/src/simulation/track/drsZones.js +67 -0
  151. package/src/simulation/track/pitLaneAccess.js +103 -0
  152. package/src/simulation/track/pitLaneLayout.js +395 -0
  153. package/src/simulation/track/pitLaneState.js +160 -0
  154. package/src/simulation/track/proceduralCenterline.js +237 -0
  155. package/src/simulation/track/sampleGeometry.js +49 -0
  156. package/src/simulation/track/spatialQueries.js +154 -0
  157. package/src/simulation/track/startStraight.js +141 -0
  158. package/src/simulation/track/trackConstants.js +109 -0
  159. package/src/simulation/track/trackGenerationOptions.js +283 -0
  160. package/src/simulation/track/trackMath.js +241 -0
  161. package/src/simulation/track/trackModel.js +240 -0
  162. package/src/simulation/track/trackQueryIndex.js +945 -0
  163. package/src/simulation/track/trackSectors.js +19 -0
  164. package/src/simulation/track/trackStatePolicy.js +24 -0
  165. package/src/simulation/track/trackValidation.js +98 -0
  166. package/src/simulation/trackModel.js +1 -1706
  167. package/src/simulation/vehicle/contactResolution.js +116 -0
  168. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  169. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  170. package/src/simulation/vehicle/runoffResponse.js +88 -0
  171. package/src/simulation/vehicle/surfacePriority.js +28 -0
  172. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  173. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  174. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  175. package/src/simulation/vehicle/vehicleState.js +186 -0
  176. package/src/simulation/vehicle/wheelSurface.js +150 -0
  177. package/src/simulation/vehicleGeometry.js +1 -247
  178. package/src/simulation/vehiclePhysics.js +1 -136
  179. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  180. package/src/simulation/wheelSurface.js +1 -338
  181. package/src/styles.css +42 -0
  182. package/src/ui/cameraControlsTemplate.js +20 -0
  183. package/src/ui/carOverviewTemplate.js +33 -0
  184. package/src/ui/componentTemplates.js +14 -360
  185. package/src/ui/raceCanvasTemplate.js +69 -0
  186. package/src/ui/raceControlsTemplate.js +36 -0
  187. package/src/ui/shellTemplate.js +5 -4
  188. package/src/ui/telemetryDrawerTemplate.js +55 -0
  189. package/src/ui/telemetryTemplates.js +136 -0
  190. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,296 @@
1
+ import { nearestTrackState } from '../../simulation/track/trackModel.js';
2
+ import { pitOverrideAllowedForCar } from '../../simulation/track/trackStatePolicy.js';
3
+ import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
4
+ import {
5
+ ANALYTIC_TRACK_RAY_MAX_CURVATURE,
6
+ DRIVER_RAY_REFINE_STEPS,
7
+ PIT_CONNECTOR_RAY_FALLBACK_METERS,
8
+ TRACK_RAY_REFINE_STEPS,
9
+ TRACK_RAY_STEP_METERS,
10
+ } from './rayDefaults.js';
11
+ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
12
+ import { pointOnRay } from './rayGeometry.js';
13
+ import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
14
+
15
+ const LEGAL_SURFACES = new Set(['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box']);
16
+ const SURFACE_CHANNELS = new Set(['kerb', 'illegalSurface']);
17
+
18
+ export function requestedSurfaceChannels(channels = []) {
19
+ return channels.filter((channel) => SURFACE_CHANNELS.has(channel));
20
+ }
21
+
22
+ export function createSurfaceMiss(lengthMeters) {
23
+ return {
24
+ hit: false,
25
+ distanceMeters: lengthMeters,
26
+ surface: null,
27
+ };
28
+ }
29
+
30
+ export function estimateSurfaceHits(
31
+ car,
32
+ snapshot,
33
+ ray,
34
+ origin,
35
+ vector,
36
+ channels,
37
+ context = null,
38
+ { precision = 'driver', sharedRayQuery = null } = {},
39
+ ) {
40
+ const requested = requestedSurfaceChannels(channels);
41
+ const misses = Object.fromEntries(requested.map((channel) => [channel, createSurfaceMiss(ray.lengthMeters)]));
42
+ if (!requested.length || !Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
43
+ return misses;
44
+ }
45
+
46
+ const originState = context?.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 };
67
+
68
+ const pending = new Set(requested);
69
+ const maxDistance = metersToSimUnits(ray.lengthMeters);
70
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
71
+ let previousDistance = 0;
72
+ let previousState = null;
73
+
74
+ for (let distance = 0; distance <= maxDistance; distance += step) {
75
+ const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress);
76
+ for (const channel of [...pending]) {
77
+ if (!matchesSurfaceChannel(channel, state)) continue;
78
+ const hitDistance = previousState
79
+ ? refineSurfaceTransition(snapshot.track, car, origin, vector, car.progress, previousDistance, distance, channel, refineStepsForPrecision(precision))
80
+ : distance;
81
+ misses[channel] = {
82
+ hit: true,
83
+ distanceMeters: simUnitsToMeters(hitDistance),
84
+ surface: state.surface ?? null,
85
+ };
86
+ pending.delete(channel);
87
+ }
88
+ if (pending.size === 0) break;
89
+ previousDistance = distance;
90
+ previousState = state;
91
+ }
92
+
93
+ return misses;
94
+ }
95
+
96
+ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState }) {
97
+ if (!originState || car.inPitLane || originState.inPitLane) return null;
98
+ if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
99
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
100
+ if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return null;
101
+
102
+ const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
103
+ if (Math.abs(lateral) < 0.08) return Object.fromEntries(requested.map((channel) => [
104
+ channel,
105
+ createSurfaceMiss(ray.lengthMeters),
106
+ ]));
107
+
108
+ const trackHalfWidth = track.width / 2;
109
+ const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
110
+ const maxDistance = metersToSimUnits(ray.lengthMeters);
111
+ const offset = originState.signedOffset ?? car.signedOffset ?? 0;
112
+ const hits = {};
113
+
114
+ requested.forEach((channel) => {
115
+ if (channel === 'kerb') {
116
+ hits[channel] = hitAbsOffsetBand({
117
+ offset,
118
+ lateral,
119
+ minAbsOffset: trackHalfWidth,
120
+ maxAbsOffset: kerbOuter,
121
+ maxDistance,
122
+ lengthMeters: ray.lengthMeters,
123
+ surface: 'kerb',
124
+ });
125
+ } else if (channel === 'illegalSurface') {
126
+ hits[channel] = hitAbsOffsetBand({
127
+ offset,
128
+ lateral,
129
+ minAbsOffset: kerbOuter,
130
+ maxAbsOffset: Infinity,
131
+ maxDistance,
132
+ lengthMeters: ray.lengthMeters,
133
+ surface: surfaceBeyondKerb(track, offset, lateral, maxDistance),
134
+ });
135
+ }
136
+ });
137
+
138
+ return hits;
139
+ }
140
+
141
+ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, sharedRayQuery }) {
142
+ if (!originState || car.inPitLane || originState.inPitLane) return null;
143
+ if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
144
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
145
+
146
+ const trackHalfWidth = track.width / 2;
147
+ const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
148
+ const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
149
+ const boundaries = findIndexedTrackBandBoundaries(
150
+ track,
151
+ origin,
152
+ vector,
153
+ ray.lengthMeters,
154
+ trackHalfWidth,
155
+ kerbOuter,
156
+ sharedRayQuery,
157
+ );
158
+ if (!boundaries.available) return null;
159
+ const hits = {};
160
+
161
+ for (const channel of requested) {
162
+ if (matchesSurfaceChannel(channel, originState)) {
163
+ hits[channel] = { hit: true, distanceMeters: 0, surface: originState.surface ?? null };
164
+ continue;
165
+ }
166
+
167
+ const boundaryDistance = channel === 'kerb'
168
+ ? minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
169
+ : boundaries.kerbOuterDistance;
170
+ if (boundaryDistance == null) return null;
171
+ hits[channel] = {
172
+ hit: true,
173
+ distanceMeters: simUnitsToMeters(boundaryDistance),
174
+ surface: channel === 'kerb'
175
+ ? 'kerb'
176
+ : surfaceBeyondKerb(track, originState.signedOffset ?? 0, lateral, metersToSimUnits(ray.lengthMeters)),
177
+ };
178
+ }
179
+
180
+ return hits;
181
+ }
182
+
183
+ function usesMainTrackOnlyRays(car) {
184
+ return car?.interaction?.profile === 'batch-training';
185
+ }
186
+
187
+ function hitAbsOffsetBand({ offset, lateral, minAbsOffset, maxAbsOffset, maxDistance, lengthMeters, surface }) {
188
+ const currentAbs = Math.abs(offset);
189
+ if (currentAbs >= minAbsOffset && currentAbs <= maxAbsOffset) {
190
+ return { hit: true, distanceMeters: 0, surface };
191
+ }
192
+
193
+ const candidates = [
194
+ distanceToOffset(minAbsOffset, offset, lateral),
195
+ distanceToOffset(-minAbsOffset, offset, lateral),
196
+ ].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance);
197
+
198
+ if (Number.isFinite(maxAbsOffset)) {
199
+ candidates.push(
200
+ ...[
201
+ distanceToOffset(maxAbsOffset, offset, lateral),
202
+ distanceToOffset(-maxAbsOffset, offset, lateral),
203
+ ].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance),
204
+ );
205
+ }
206
+
207
+ const distance = candidates
208
+ .filter((candidate) => {
209
+ const nextAbs = Math.abs(offset + lateral * candidate);
210
+ return nextAbs >= minAbsOffset - 1e-6 && nextAbs <= maxAbsOffset + 1e-6;
211
+ })
212
+ .sort((a, b) => a - b)[0];
213
+
214
+ if (!Number.isFinite(distance)) return createSurfaceMiss(lengthMeters);
215
+ return { hit: true, distanceMeters: simUnitsToMeters(distance), surface };
216
+ }
217
+
218
+ function distanceToOffset(targetOffset, offset, lateral) {
219
+ return (targetOffset - offset) / lateral;
220
+ }
221
+
222
+ function surfaceBeyondKerb(track, offset, lateral, maxDistance) {
223
+ const gravelOuter = track.width / 2 + (track.kerbWidth ?? 0) + (track.gravelWidth ?? 0);
224
+ const hitDistance = Math.min(
225
+ ...[
226
+ distanceToOffset(track.width / 2 + (track.kerbWidth ?? 0), offset, lateral),
227
+ distanceToOffset(-track.width / 2 - (track.kerbWidth ?? 0), offset, lateral),
228
+ ].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance),
229
+ );
230
+ const hitAbs = Math.abs(offset + lateral * hitDistance);
231
+ return hitAbs <= gravelOuter ? 'gravel' : 'grass';
232
+ }
233
+
234
+ function isNearPitConnector(track, state) {
235
+ const pitLane = track.pitLane;
236
+ if (!pitLane?.enabled || !Number.isFinite(state?.distance)) return false;
237
+ const window = metersToSimUnits(PIT_CONNECTOR_RAY_FALLBACK_METERS);
238
+ const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
239
+ const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
240
+ return wrappedTrackDistance(state.distance, entryDistance, track.length) <= window ||
241
+ wrappedTrackDistance(state.distance, exitDistance, track.length) <= window;
242
+ }
243
+
244
+ function wrappedTrackDistance(first, second, totalLength) {
245
+ if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(totalLength) || totalLength <= 0) {
246
+ return Infinity;
247
+ }
248
+ const delta = Math.abs(first - second);
249
+ return Math.min(delta, totalLength - delta);
250
+ }
251
+
252
+ function refineSurfaceTransition(
253
+ track,
254
+ car,
255
+ origin,
256
+ ray,
257
+ progressHint,
258
+ lowDistance,
259
+ highDistance,
260
+ channel,
261
+ refineSteps = TRACK_RAY_REFINE_STEPS,
262
+ ) {
263
+ let low = lowDistance;
264
+ let high = highDistance;
265
+ for (let index = 0; index < refineSteps; index += 1) {
266
+ const middle = (low + high) / 2;
267
+ const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint);
268
+ if (matchesSurfaceChannel(channel, state)) high = middle;
269
+ else low = middle;
270
+ }
271
+ return high;
272
+ }
273
+
274
+ function refineStepsForPrecision(precision) {
275
+ return precision === 'debug' ? TRACK_RAY_REFINE_STEPS : DRIVER_RAY_REFINE_STEPS;
276
+ }
277
+
278
+ function matchesSurfaceChannel(channel, state) {
279
+ if (channel === 'kerb') return state.surface === 'kerb';
280
+ if (channel === 'illegalSurface') return !LEGAL_SURFACES.has(state.surface) && state.surface !== 'barrier';
281
+ return false;
282
+ }
283
+
284
+ function nearestRayTrackState(track, car, point, progressHint) {
285
+ const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
286
+ return nearestTrackState(track, point, progressHint, {
287
+ allowPitOverride: pitOverrideAllowedForCar(car),
288
+ indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
289
+ hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
290
+ });
291
+ }
292
+
293
+ function minFinite(...values) {
294
+ const minimum = Math.min(...values.filter((value) => Number.isFinite(value)));
295
+ return Number.isFinite(minimum) ? minimum : null;
296
+ }
@@ -0,0 +1,261 @@
1
+ import { nearestTrackState } from '../../simulation/track/trackModel.js';
2
+ import { pitOverrideAllowedForCar } from '../../simulation/track/trackStatePolicy.js';
3
+ import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
4
+ import {
5
+ ANALYTIC_TRACK_RAY_MAX_CURVATURE,
6
+ DRIVER_RAY_REFINE_STEPS,
7
+ PIT_CONNECTOR_RAY_FALLBACK_METERS,
8
+ TRACK_RAY_REFINE_STEPS,
9
+ TRACK_RAY_STEP_METERS,
10
+ } from './rayDefaults.js';
11
+ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
12
+ import { degreesToRadians, getCarRayOrigin, getCarRayVector, pointOnRay } from './rayGeometry.js';
13
+ import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
14
+
15
+ export function createTrackRayContext(car, snapshot, origin) {
16
+ if (!Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
17
+ return { origin, originState: null };
18
+ }
19
+ return {
20
+ origin,
21
+ originState: nearestRayTrackState(snapshot.track, car, origin, car.progress),
22
+ };
23
+ }
24
+
25
+ export function estimateTrackHit(
26
+ car,
27
+ snapshot,
28
+ angleDegrees,
29
+ lengthMeters,
30
+ context = null,
31
+ { precision = 'driver', sharedRayQuery = null } = {},
32
+ ) {
33
+ if (!Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
34
+ return estimateLocalTrackHit(car, snapshot, angleDegrees, lengthMeters);
35
+ }
36
+
37
+ const origin = context?.origin ?? getCarRayOrigin(car);
38
+ const ray = getCarRayVector(car, angleDegrees);
39
+ const maxDistance = metersToSimUnits(lengthMeters);
40
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
41
+ const originState = context?.originState ?? nearestRayTrackState(snapshot.track, car, origin, car.progress);
42
+ 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;
54
+ const analyticHit = estimateAnalyticMainTrackHit({
55
+ car,
56
+ track: snapshot.track,
57
+ origin,
58
+ originState,
59
+ ray,
60
+ lengthMeters,
61
+ includePitLane,
62
+ });
63
+ if (analyticHit) return analyticHit;
64
+ let previousDistance = 0;
65
+ let previousInside = null;
66
+
67
+ for (let distance = 0; distance <= maxDistance; distance += step) {
68
+ const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, ray, distance), car.progress);
69
+ const inside = isInsideTrackBorder(state, snapshot.track, includePitLane);
70
+ if (previousInside == null) {
71
+ previousInside = inside;
72
+ previousDistance = distance;
73
+ continue;
74
+ }
75
+
76
+ if (inside !== previousInside) {
77
+ const kind = previousInside ? 'exit' : 'entry';
78
+ const hitDistance = refineTrackTransitionDistance(
79
+ snapshot.track,
80
+ car,
81
+ origin,
82
+ ray,
83
+ car.progress,
84
+ previousDistance,
85
+ distance,
86
+ kind,
87
+ includePitLane,
88
+ refineStepsForPrecision(precision),
89
+ );
90
+ return {
91
+ hit: true,
92
+ distanceMeters: simUnitsToMeters(hitDistance),
93
+ kind,
94
+ };
95
+ }
96
+ previousDistance = distance;
97
+ previousInside = inside;
98
+ }
99
+
100
+ return createTrackMiss(lengthMeters);
101
+ }
102
+
103
+ export function createTrackMiss(lengthMeters) {
104
+ return {
105
+ hit: false,
106
+ distanceMeters: lengthMeters,
107
+ kind: null,
108
+ };
109
+ }
110
+
111
+ function estimateAnalyticMainTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane }) {
112
+ if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return null;
113
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
114
+ if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return null;
115
+
116
+ const lateral = ray.x * originState.normalX + ray.y * originState.normalY;
117
+ if (Math.abs(lateral) < 0.08) return createTrackMiss(lengthMeters);
118
+
119
+ const trackHalfWidth = track.width / 2;
120
+ const offset = originState.signedOffset ?? car.signedOffset ?? 0;
121
+ const inside = Math.abs(offset) <= trackHalfWidth;
122
+ const targetEdge = getLocalTrackTransitionTarget({
123
+ inside,
124
+ offsetMeters: offset,
125
+ lateral,
126
+ trackHalfWidthMeters: trackHalfWidth,
127
+ });
128
+ if (targetEdge == null) return createTrackMiss(lengthMeters);
129
+
130
+ const distance = (targetEdge - offset) / lateral;
131
+ const maxDistance = metersToSimUnits(lengthMeters);
132
+ if (distance < 0 || distance > maxDistance) return createTrackMiss(lengthMeters);
133
+
134
+ return {
135
+ hit: true,
136
+ distanceMeters: simUnitsToMeters(distance),
137
+ kind: inside ? 'exit' : 'entry',
138
+ };
139
+ }
140
+
141
+ function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane, sharedRayQuery }) {
142
+ if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return null;
143
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
144
+ const trackHalfWidth = track.width / 2;
145
+ const inside = Math.abs(originState.signedOffset ?? 0) <= trackHalfWidth;
146
+ const boundaries = findIndexedTrackBandBoundaries(
147
+ track,
148
+ origin,
149
+ ray,
150
+ lengthMeters,
151
+ trackHalfWidth,
152
+ trackHalfWidth + (track.kerbWidth ?? 0),
153
+ sharedRayQuery,
154
+ );
155
+ if (!boundaries.available) return null;
156
+ if (boundaries.trackEdgeDistance == null) return null;
157
+ return {
158
+ hit: true,
159
+ distanceMeters: simUnitsToMeters(boundaries.trackEdgeDistance),
160
+ kind: inside ? 'exit' : 'entry',
161
+ };
162
+ }
163
+
164
+ function usesMainTrackOnlyRays(car) {
165
+ return car?.interaction?.profile === 'batch-training';
166
+ }
167
+
168
+ function isNearPitConnector(track, state) {
169
+ const pitLane = track.pitLane;
170
+ if (!pitLane?.enabled || !Number.isFinite(state?.distance)) return false;
171
+ const window = metersToSimUnits(PIT_CONNECTOR_RAY_FALLBACK_METERS);
172
+ const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
173
+ const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
174
+ return wrappedTrackDistance(state.distance, entryDistance, track.length) <= window ||
175
+ wrappedTrackDistance(state.distance, exitDistance, track.length) <= window;
176
+ }
177
+
178
+ function wrappedTrackDistance(first, second, totalLength) {
179
+ if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(totalLength) || totalLength <= 0) {
180
+ return Infinity;
181
+ }
182
+ const delta = Math.abs(first - second);
183
+ return Math.min(delta, totalLength - delta);
184
+ }
185
+
186
+ function estimateLocalTrackHit(car, snapshot, angleDegrees, lengthMeters) {
187
+ const trackHalfWidthMeters = simUnitsToMeters(snapshot.track.width / 2);
188
+ const offsetMeters = simUnitsToMeters(car.signedOffset ?? 0);
189
+ const lateral = Math.sin(degreesToRadians(angleDegrees));
190
+ if (Math.abs(lateral) < 0.08) {
191
+ return createTrackMiss(lengthMeters);
192
+ }
193
+
194
+ const inside = Math.abs(offsetMeters) <= trackHalfWidthMeters;
195
+ const targetEdge = getLocalTrackTransitionTarget({ inside, offsetMeters, lateral, trackHalfWidthMeters });
196
+ if (targetEdge == null) return createTrackMiss(lengthMeters);
197
+
198
+ const distanceMeters = (targetEdge - offsetMeters) / lateral;
199
+ if (distanceMeters < 0 || distanceMeters > lengthMeters) return createTrackMiss(lengthMeters);
200
+
201
+ return {
202
+ hit: true,
203
+ distanceMeters,
204
+ kind: inside ? 'exit' : 'entry',
205
+ };
206
+ }
207
+
208
+ function getLocalTrackTransitionTarget({ inside, offsetMeters, lateral, trackHalfWidthMeters }) {
209
+ if (inside) return lateral > 0 ? trackHalfWidthMeters : -trackHalfWidthMeters;
210
+ if (offsetMeters > trackHalfWidthMeters) return lateral < 0 ? trackHalfWidthMeters : null;
211
+ if (offsetMeters < -trackHalfWidthMeters) return lateral > 0 ? -trackHalfWidthMeters : null;
212
+ return null;
213
+ }
214
+
215
+ function refineTrackTransitionDistance(
216
+ track,
217
+ car,
218
+ origin,
219
+ ray,
220
+ progressHint,
221
+ lowDistance,
222
+ highDistance,
223
+ kind,
224
+ includePitLane = true,
225
+ refineSteps = TRACK_RAY_REFINE_STEPS,
226
+ ) {
227
+ let low = lowDistance;
228
+ let high = highDistance;
229
+ for (let index = 0; index < refineSteps; index += 1) {
230
+ const middle = (low + high) / 2;
231
+ const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint);
232
+ const inside = isInsideTrackBorder(state, track, includePitLane);
233
+ if (kind === 'entry') {
234
+ if (inside) high = middle;
235
+ else low = middle;
236
+ } else if (inside) {
237
+ low = middle;
238
+ } else {
239
+ high = middle;
240
+ }
241
+ }
242
+ return high;
243
+ }
244
+
245
+ function nearestRayTrackState(track, car, point, progressHint) {
246
+ const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
247
+ return nearestTrackState(track, point, progressHint, {
248
+ allowPitOverride: pitOverrideAllowedForCar(car),
249
+ indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
250
+ hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
251
+ });
252
+ }
253
+
254
+ function refineStepsForPrecision(precision) {
255
+ return precision === 'debug' ? TRACK_RAY_REFINE_STEPS : DRIVER_RAY_REFINE_STEPS;
256
+ }
257
+
258
+ function isInsideTrackBorder(state, track, includePitLane = true) {
259
+ if (includePitLane && state.inPitLane) return true;
260
+ return state.crossTrackError <= track.width / 2;
261
+ }