@inventure71/paddockjs 0.3.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 (202) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/INSTALL_AND_UPDATE.md +5 -2
  3. package/README.md +328 -13
  4. package/docs/architecture.md +150 -41
  5. package/docs/concepts.md +22 -3
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +474 -34
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +1 -0
  10. package/docs/learnings.md +23 -1
  11. package/docs/rules.md +169 -21
  12. package/docs/system_specs.md +56 -20
  13. package/docs/training.md +369 -25
  14. package/examples/python/README.md +62 -0
  15. package/examples/python/base_policy_server.py +191 -0
  16. package/examples/python/requirements.txt +2 -0
  17. package/examples/train-basic-policy.mjs +5 -6
  18. package/examples/trainingData.mjs +133 -0
  19. package/package.json +15 -4
  20. package/src/api/PaddockSimulatorController.js +36 -2
  21. package/src/app/BrowserExpertAdapter.js +367 -9
  22. package/src/app/F1SimulatorApp.js +615 -940
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +14 -1
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +24 -4
  48. package/src/environment/actions.js +38 -7
  49. package/src/environment/controllerLoop.js +246 -0
  50. package/src/environment/episode.js +86 -2
  51. package/src/environment/evaluation.js +206 -0
  52. package/src/environment/index.d.ts +638 -6
  53. package/src/environment/index.js +14 -0
  54. package/src/environment/metrics.js +159 -0
  55. package/src/environment/observationOptions.js +33 -0
  56. package/src/environment/observations.js +500 -43
  57. package/src/environment/options.js +41 -2
  58. package/src/environment/recorder.js +28 -0
  59. package/src/environment/runtime.js +260 -24
  60. package/src/environment/scenarios.js +249 -0
  61. package/src/environment/sensors/bodySenses.js +23 -0
  62. package/src/environment/sensors/boundarySenses.js +17 -0
  63. package/src/environment/sensors/carRays.js +75 -0
  64. package/src/environment/sensors/contactSenses.js +32 -0
  65. package/src/environment/sensors/index.js +157 -0
  66. package/src/environment/sensors/indexedRayBands.js +103 -0
  67. package/src/environment/sensors/nearbyCars.js +52 -0
  68. package/src/environment/sensors/opponentRadar.js +65 -0
  69. package/src/environment/sensors/rayConfig.js +110 -0
  70. package/src/environment/sensors/rayDefaults.js +6 -0
  71. package/src/environment/sensors/rayGeometry.js +60 -0
  72. package/src/environment/sensors/rayGuards.js +19 -0
  73. package/src/environment/sensors/sensorTargets.js +60 -0
  74. package/src/environment/sensors/surfaceRays.js +296 -0
  75. package/src/environment/sensors/trackRays.js +261 -0
  76. package/src/environment/sensors.js +1 -257
  77. package/src/environment/specs.js +188 -9
  78. package/src/environment/workerProtocol.js +64 -0
  79. package/src/index.d.ts +533 -7
  80. package/src/index.js +26 -0
  81. package/src/rendering/proceduralTrackAsset.js +20 -510
  82. package/src/rendering/renderSnapshot.js +38 -12
  83. package/src/rendering/track/finishLineRenderer.js +34 -0
  84. package/src/rendering/track/gridRenderer.js +47 -0
  85. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  86. package/src/rendering/track/pitLaneRenderer.js +248 -0
  87. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  88. package/src/rendering/track/trackRenderConstants.js +38 -0
  89. package/src/rendering/track/trackRenderGeometry.js +94 -0
  90. package/src/simulation/collisionGeometry.js +214 -0
  91. package/src/simulation/driver/driverControlConstants.js +28 -0
  92. package/src/simulation/driver/driverController.js +47 -0
  93. package/src/simulation/driver/driverInput.js +28 -0
  94. package/src/simulation/driver/driverMath.js +6 -0
  95. package/src/simulation/driver/driverPersonality.js +25 -0
  96. package/src/simulation/driver/edgeRecovery.js +48 -0
  97. package/src/simulation/driver/racingControls.js +399 -0
  98. package/src/simulation/driver/racingLinePlan.js +166 -0
  99. package/src/simulation/driver/recoveryDynamics.js +38 -0
  100. package/src/simulation/driver/rejoinControls.js +213 -0
  101. package/src/simulation/driver/safetyCarControls.js +35 -0
  102. package/src/simulation/driver/trafficScan.js +51 -0
  103. package/src/simulation/driverController.js +4 -253
  104. package/src/simulation/participants/participantInteractions.js +120 -0
  105. package/src/simulation/pit/pitFlow.js +234 -0
  106. package/src/simulation/pit/pitIntent.js +92 -0
  107. package/src/simulation/pit/pitOccupancy.js +47 -0
  108. package/src/simulation/pit/pitPenaltyService.js +71 -0
  109. package/src/simulation/pit/pitQueue.js +55 -0
  110. package/src/simulation/pit/pitRouteMovement.js +160 -0
  111. package/src/simulation/pit/pitRouting.js +319 -0
  112. package/src/simulation/pit/pitService.js +41 -0
  113. package/src/simulation/pit/pitServiceConstants.js +21 -0
  114. package/src/simulation/pit/pitServiceProfile.js +45 -0
  115. package/src/simulation/pit/pitSnapshots.js +61 -0
  116. package/src/simulation/pit/pitState.js +113 -0
  117. package/src/simulation/pit/pitTireService.js +92 -0
  118. package/src/simulation/race/classification.js +104 -0
  119. package/src/simulation/race/gridPenalties.js +37 -0
  120. package/src/simulation/race/raceControlState.js +38 -0
  121. package/src/simulation/race/raceDistance.js +32 -0
  122. package/src/simulation/race/raceFinish.js +65 -0
  123. package/src/simulation/race/raceLifecycle.js +71 -0
  124. package/src/simulation/race/raceOrder.js +113 -0
  125. package/src/simulation/race/raceProgress.js +124 -0
  126. package/src/simulation/race/raceSetup.js +93 -0
  127. package/src/simulation/race/raceStep.js +80 -0
  128. package/src/simulation/race/redFlag.js +15 -0
  129. package/src/simulation/race/retirements.js +44 -0
  130. package/src/simulation/race/safetyCar.js +86 -0
  131. package/src/simulation/race/stalledDnf.js +86 -0
  132. package/src/simulation/raceSimulation.js +386 -1012
  133. package/src/simulation/replay/replayGhosts.js +128 -0
  134. package/src/simulation/rules/collisionSteward.js +69 -0
  135. package/src/simulation/rules/moduleConfig.js +72 -0
  136. package/src/simulation/rules/penaltyConfig.js +99 -0
  137. package/src/simulation/rules/penaltyEvents.js +18 -0
  138. package/src/simulation/rules/penaltyLedger.js +153 -0
  139. package/src/simulation/rules/penaltyStats.js +26 -0
  140. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  141. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  142. package/src/simulation/rules/ruleDefaults.js +85 -0
  143. package/src/simulation/rules/rulesReview.js +141 -0
  144. package/src/simulation/rules/rulesetPresets.js +47 -0
  145. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  147. package/src/simulation/rulesConfig.js +34 -0
  148. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryIndex.js +945 -0
  171. package/src/simulation/track/trackSectors.js +19 -0
  172. package/src/simulation/track/trackStatePolicy.js +24 -0
  173. package/src/simulation/track/trackValidation.js +98 -0
  174. package/src/simulation/trackModel.js +1 -628
  175. package/src/simulation/units.js +6 -4
  176. package/src/simulation/vehicle/contactResolution.js +116 -0
  177. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  178. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  179. package/src/simulation/vehicle/runoffResponse.js +88 -0
  180. package/src/simulation/vehicle/surfacePriority.js +28 -0
  181. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  182. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  183. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  184. package/src/simulation/vehicle/vehicleState.js +186 -0
  185. package/src/simulation/vehicle/wheelSurface.js +150 -0
  186. package/src/simulation/vehicleGeometry.js +1 -0
  187. package/src/simulation/vehiclePhysics.js +1 -104
  188. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  189. package/src/simulation/wheelSurface.js +1 -0
  190. package/src/styles.css +342 -42
  191. package/src/ui/bannerTemplates.js +69 -0
  192. package/src/ui/cameraControlsTemplate.js +20 -0
  193. package/src/ui/carOverviewTemplate.js +33 -0
  194. package/src/ui/componentTemplates.js +20 -426
  195. package/src/ui/raceCanvasTemplate.js +69 -0
  196. package/src/ui/raceControlStatusBanner.js +39 -0
  197. package/src/ui/raceControlsTemplate.js +36 -0
  198. package/src/ui/shellTemplate.js +6 -5
  199. package/src/ui/telemetryDrawerTemplate.js +55 -0
  200. package/src/ui/telemetryTemplates.js +136 -0
  201. package/src/ui/templateUtils.js +23 -0
  202. package/src/ui/timingTowerTemplate.js +34 -0
@@ -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
+ }
@@ -1,257 +1 @@
1
- import { nearestTrackState } from '../simulation/trackModel.js';
2
- import { metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
3
- import { VEHICLE_LIMITS } from '../simulation/vehiclePhysics.js';
4
-
5
- const TRACK_RAY_STEP_METERS = 1;
6
- const TRACK_RAY_REFINE_STEPS = 12;
7
- export const DEFAULT_RAY_ANGLES_DEGREES = [-135, -60, -20, 0, 20, 60, 135, 180];
8
-
9
- export function buildNearbyCars(car, snapshot, { maxCars = 6, radiusMeters = 150 } = {}) {
10
- return snapshot.cars
11
- .filter((other) => other.id !== car.id)
12
- .map((other) => {
13
- const dx = other.x - car.x;
14
- const dy = other.y - car.y;
15
- const distanceMeters = simUnitsToMeters(Math.hypot(dx, dy));
16
- const forward = Math.cos(car.heading) * dx + Math.sin(car.heading) * dy;
17
- const right = -Math.sin(car.heading) * dx + Math.cos(car.heading) * dy;
18
- return {
19
- id: other.id,
20
- relativeForwardMeters: simUnitsToMeters(forward),
21
- relativeRightMeters: simUnitsToMeters(right),
22
- relativeDistanceMeters: distanceMeters,
23
- relativeSpeedKph: other.speedKph - car.speedKph,
24
- relativeHeadingRadians: normalizeRelativeHeading(other.heading - car.heading),
25
- ahead: forward > 0,
26
- sameLap: other.lap === car.lap,
27
- };
28
- })
29
- .filter((entry) => entry.relativeDistanceMeters <= radiusMeters)
30
- .sort((a, b) => a.relativeDistanceMeters - b.relativeDistanceMeters)
31
- .slice(0, maxCars);
32
- }
33
-
34
- export function buildRaySensors(car, snapshot, rayOptions = {}) {
35
- const angles = rayOptions.anglesDegrees ?? DEFAULT_RAY_ANGLES_DEGREES;
36
- const lengthMeters = rayOptions.lengthMeters ?? 120;
37
-
38
- return angles.map((angleDegrees) => ({
39
- angleDegrees,
40
- angleRadians: degreesToRadians(angleDegrees),
41
- lengthMeters,
42
- track: rayOptions.detectTrack === false
43
- ? createTrackMiss(lengthMeters)
44
- : estimateTrackHit(car, snapshot, angleDegrees, lengthMeters),
45
- car: rayOptions.detectCars === false
46
- ? { hit: false, distanceMeters: lengthMeters, driverId: null, relativeSpeedKph: 0 }
47
- : estimateCarHit(car, snapshot, angleDegrees, lengthMeters),
48
- }));
49
- }
50
-
51
- function estimateTrackHit(car, snapshot, angleDegrees, lengthMeters) {
52
- if (!Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
53
- return estimateLocalTrackHit(car, snapshot, angleDegrees, lengthMeters);
54
- }
55
-
56
- const origin = getCarRayOrigin(car);
57
- const ray = getCarRayVector(car, angleDegrees);
58
- const maxDistance = metersToSimUnits(lengthMeters);
59
- const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
60
- let previousDistance = 0;
61
- let previousInside = null;
62
-
63
- for (let distance = 0; distance <= maxDistance; distance += step) {
64
- const state = nearestTrackState(snapshot.track, pointOnRay(origin, ray, distance), car.progress);
65
- const inside = isInsideTrackBorder(state, snapshot.track);
66
- if (previousInside == null) {
67
- previousInside = inside;
68
- previousDistance = distance;
69
- continue;
70
- }
71
-
72
- if (inside !== previousInside) {
73
- const kind = previousInside ? 'exit' : 'entry';
74
- const hitDistance = refineTrackTransitionDistance(
75
- snapshot.track,
76
- origin,
77
- ray,
78
- car.progress,
79
- previousDistance,
80
- distance,
81
- kind,
82
- );
83
- return {
84
- hit: true,
85
- distanceMeters: simUnitsToMeters(hitDistance),
86
- kind,
87
- };
88
- }
89
- previousDistance = distance;
90
- previousInside = inside;
91
- }
92
-
93
- return createTrackMiss(lengthMeters);
94
- }
95
-
96
- function estimateLocalTrackHit(car, snapshot, angleDegrees, lengthMeters) {
97
- const trackHalfWidthMeters = simUnitsToMeters(snapshot.track.width / 2);
98
- const offsetMeters = simUnitsToMeters(car.signedOffset ?? 0);
99
- const lateral = Math.sin(degreesToRadians(angleDegrees));
100
- if (Math.abs(lateral) < 0.08) {
101
- return createTrackMiss(lengthMeters);
102
- }
103
-
104
- const inside = Math.abs(offsetMeters) <= trackHalfWidthMeters;
105
- const targetEdge = getLocalTrackTransitionTarget({ inside, offsetMeters, lateral, trackHalfWidthMeters });
106
- if (targetEdge == null) return createTrackMiss(lengthMeters);
107
-
108
- const distanceMeters = (targetEdge - offsetMeters) / lateral;
109
- if (distanceMeters < 0 || distanceMeters > lengthMeters) return createTrackMiss(lengthMeters);
110
-
111
- return {
112
- hit: true,
113
- distanceMeters,
114
- kind: inside ? 'exit' : 'entry',
115
- };
116
- }
117
-
118
- function getLocalTrackTransitionTarget({ inside, offsetMeters, lateral, trackHalfWidthMeters }) {
119
- if (inside) return lateral > 0 ? trackHalfWidthMeters : -trackHalfWidthMeters;
120
- if (offsetMeters > trackHalfWidthMeters) return lateral < 0 ? trackHalfWidthMeters : null;
121
- if (offsetMeters < -trackHalfWidthMeters) return lateral > 0 ? -trackHalfWidthMeters : null;
122
- return null;
123
- }
124
-
125
- function refineTrackTransitionDistance(track, origin, ray, progressHint, lowDistance, highDistance, kind) {
126
- let low = lowDistance;
127
- let high = highDistance;
128
- for (let index = 0; index < TRACK_RAY_REFINE_STEPS; index += 1) {
129
- const middle = (low + high) / 2;
130
- const state = nearestTrackState(track, pointOnRay(origin, ray, middle), progressHint);
131
- const inside = isInsideTrackBorder(state, track);
132
- if (kind === 'entry') {
133
- if (inside) high = middle;
134
- else low = middle;
135
- } else if (inside) {
136
- low = middle;
137
- } else {
138
- high = middle;
139
- }
140
- }
141
- return high;
142
- }
143
-
144
- function isInsideTrackBorder(state, track) {
145
- return state.crossTrackError <= track.width / 2;
146
- }
147
-
148
- function createTrackMiss(lengthMeters) {
149
- return {
150
- hit: false,
151
- distanceMeters: lengthMeters,
152
- kind: null,
153
- };
154
- }
155
-
156
- function estimateCarHit(car, snapshot, angleDegrees, lengthMeters) {
157
- const origin = getCarRayOrigin(car);
158
- const ray = getCarRayVector(car, angleDegrees);
159
- const maxDistance = metersToSimUnits(lengthMeters);
160
- let closest = null;
161
- snapshot.cars.forEach((other) => {
162
- if (other.id === car.id) return;
163
- const hitDistance = intersectCarFootprint(origin, ray, other);
164
- if (hitDistance == null || hitDistance > maxDistance) return;
165
- const distanceMeters = simUnitsToMeters(hitDistance);
166
- if (!closest || hitDistance < closest.distanceSimUnits) {
167
- closest = {
168
- hit: true,
169
- distanceSimUnits: hitDistance,
170
- distanceMeters,
171
- driverId: other.id,
172
- relativeSpeedKph: other.speedKph - car.speedKph,
173
- };
174
- }
175
- });
176
- if (!closest) return { hit: false, distanceMeters: lengthMeters, driverId: null, relativeSpeedKph: 0 };
177
- const { distanceSimUnits, ...publicHit } = closest;
178
- return publicHit;
179
- }
180
-
181
- export function getCarRayOrigin(car) {
182
- return {
183
- x: car.x,
184
- y: car.y,
185
- };
186
- }
187
-
188
- export function getCarRayVector(car, angleDegrees) {
189
- const angle = car.heading + degreesToRadians(angleDegrees);
190
- return {
191
- x: Math.cos(angle),
192
- y: Math.sin(angle),
193
- };
194
- }
195
-
196
- function pointOnRay(origin, ray, distance) {
197
- return {
198
- x: origin.x + ray.x * distance,
199
- y: origin.y + ray.y * distance,
200
- };
201
- }
202
-
203
- function intersectCarFootprint(origin, ray, other) {
204
- const forward = { x: Math.cos(other.heading), y: Math.sin(other.heading) };
205
- const right = { x: -Math.sin(other.heading), y: Math.cos(other.heading) };
206
- const delta = { x: origin.x - other.x, y: origin.y - other.y };
207
- const localOrigin = {
208
- x: dot(delta, forward),
209
- y: dot(delta, right),
210
- };
211
- const localRay = {
212
- x: dot(ray, forward),
213
- y: dot(ray, right),
214
- };
215
- const halfLength = VEHICLE_LIMITS.carLength / 2;
216
- const halfWidth = VEHICLE_LIMITS.carWidth / 2;
217
- return intersectAxisAlignedBoxRay(localOrigin, localRay, halfLength, halfWidth);
218
- }
219
-
220
- function intersectAxisAlignedBoxRay(origin, ray, halfLength, halfWidth) {
221
- let tMin = -Infinity;
222
- let tMax = Infinity;
223
- const xRange = intersectSlab(origin.x, ray.x, -halfLength, halfLength);
224
- const yRange = intersectSlab(origin.y, ray.y, -halfWidth, halfWidth);
225
- if (!xRange || !yRange) return null;
226
- tMin = Math.max(tMin, xRange.min, yRange.min);
227
- tMax = Math.min(tMax, xRange.max, yRange.max);
228
- if (tMax < 0 || tMin > tMax) return null;
229
- return Math.max(0, tMin);
230
- }
231
-
232
- function intersectSlab(origin, direction, min, max) {
233
- if (Math.abs(direction) < 1e-9) {
234
- return origin >= min && origin <= max ? { min: -Infinity, max: Infinity } : null;
235
- }
236
- const first = (min - origin) / direction;
237
- const second = (max - origin) / direction;
238
- return {
239
- min: Math.min(first, second),
240
- max: Math.max(first, second),
241
- };
242
- }
243
-
244
- function dot(a, b) {
245
- return a.x * b.x + a.y * b.y;
246
- }
247
-
248
- function degreesToRadians(degrees) {
249
- return (degrees * Math.PI) / 180;
250
- }
251
-
252
- function normalizeRelativeHeading(angle) {
253
- let value = angle;
254
- while (value > Math.PI) value -= Math.PI * 2;
255
- while (value < -Math.PI) value += Math.PI * 2;
256
- return value;
257
- }
1
+ export * from './sensors/index.js';