@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,182 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { TELEMETRY_SECTOR_COUNT } from './timingConstants.js';
3
+ import {
4
+ createEmptySectorPerformance,
5
+ createEmptySectorTimes,
6
+ finiteOrNull,
7
+ serializeSectorPerformance,
8
+ serializeSectorTimes,
9
+ } from './sectorPerformance.js';
10
+
11
+ function getSectorLength(track) {
12
+ return track.length / TELEMETRY_SECTOR_COUNT;
13
+ }
14
+
15
+ function getLapTelemetryPosition(track, raceDistance, totalLaps = Infinity) {
16
+ const positiveDistance = Math.max(0, raceDistance ?? 0);
17
+ const lapIndex = Math.min(
18
+ Math.floor(positiveDistance / track.length),
19
+ Math.max(0, totalLaps - 1),
20
+ );
21
+ const lapProgress = positiveDistance >= track.length * totalLaps
22
+ ? track.length
23
+ : positiveDistance - lapIndex * track.length;
24
+ const sectorLength = getSectorLength(track);
25
+ const sectorIndex = Math.min(TELEMETRY_SECTOR_COUNT - 1, Math.floor(lapProgress / sectorLength));
26
+ const sectorStart = sectorIndex * sectorLength;
27
+
28
+ return {
29
+ completedLaps: Math.floor(positiveDistance / track.length),
30
+ currentLap: Math.max(1, lapIndex + 1),
31
+ currentSector: sectorIndex + 1,
32
+ currentSectorProgress: clamp((lapProgress - sectorStart) / sectorLength, 0, 1),
33
+ };
34
+ }
35
+
36
+ function createSectorProgress(position, currentSectors = null) {
37
+ const activeIndex = clamp((position.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
38
+ return createEmptySectorTimes().map((_, index) => {
39
+ if (index < activeIndex && Number.isFinite(currentSectors?.[index])) return 1;
40
+ if (index === activeIndex) return clamp(position.currentSectorProgress ?? 0, 0, 1);
41
+ return 0;
42
+ });
43
+ }
44
+
45
+ export function createLapTelemetry(track, currentTime = 0, raceDistance = 0, totalLaps = Infinity) {
46
+ const position = getLapTelemetryPosition(track, raceDistance, totalLaps);
47
+ return {
48
+ ...position,
49
+ currentLapStartedAt: currentTime,
50
+ currentSectorStartedAt: currentTime,
51
+ lastUpdatedAt: currentTime,
52
+ currentLapTime: 0,
53
+ currentSectorElapsed: 0,
54
+ currentSectors: createEmptySectorTimes(),
55
+ sectorProgress: createSectorProgress(position),
56
+ liveSectors: createEmptySectorTimes(),
57
+ lastLapTime: null,
58
+ bestLapTime: null,
59
+ lastSectors: createEmptySectorTimes(),
60
+ bestSectors: createEmptySectorTimes(),
61
+ sectorPerformance: createEmptySectorPerformance(),
62
+ };
63
+ }
64
+
65
+ export function resetLapTelemetry(car, currentTime, track, totalLaps) {
66
+ car.lapTelemetry = createLapTelemetry(track, currentTime, car.raceDistance, totalLaps);
67
+ }
68
+
69
+ export function serializeLapTelemetry(telemetry) {
70
+ return {
71
+ currentLap: telemetry.currentLap,
72
+ currentSector: telemetry.currentSector,
73
+ currentLapTime: finiteOrNull(telemetry.currentLapTime),
74
+ currentSectorElapsed: finiteOrNull(telemetry.currentSectorElapsed),
75
+ currentSectorProgress: finiteOrNull(telemetry.currentSectorProgress),
76
+ currentSectors: serializeSectorTimes(telemetry.currentSectors),
77
+ sectorProgress: serializeSectorTimes(telemetry.sectorProgress),
78
+ liveSectors: serializeSectorTimes(telemetry.liveSectors),
79
+ lastSectors: serializeSectorTimes(telemetry.lastSectors),
80
+ bestSectors: serializeSectorTimes(telemetry.bestSectors),
81
+ lastLapTime: finiteOrNull(telemetry.lastLapTime),
82
+ bestLapTime: finiteOrNull(telemetry.bestLapTime),
83
+ sectorPerformance: {
84
+ current: serializeSectorPerformance(telemetry.sectorPerformance?.current),
85
+ last: serializeSectorPerformance(telemetry.sectorPerformance?.last),
86
+ best: serializeSectorPerformance(telemetry.sectorPerformance?.best),
87
+ },
88
+ completedLaps: telemetry.completedLaps,
89
+ };
90
+ }
91
+
92
+ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track, totalLaps) {
93
+ if (!car.lapTelemetry) resetLapTelemetry(car, currentTime, track, totalLaps);
94
+ const telemetry = car.lapTelemetry;
95
+ const currentRaceDistance = car.raceDistance ?? 0;
96
+ const previousDistance = Math.max(0, previousRaceDistance ?? currentRaceDistance);
97
+ const currentDistance = Math.max(0, currentRaceDistance);
98
+ const travelled = currentDistance - previousDistance;
99
+ const previousUpdateTime = Number.isFinite(telemetry.lastUpdatedAt) ? telemetry.lastUpdatedAt : currentTime;
100
+ const elapsedTime = Math.max(0, currentTime - previousUpdateTime);
101
+
102
+ if (!Number.isFinite(previousDistance) || !Number.isFinite(currentDistance) || travelled < -1e-3 || travelled > track.length / 2) {
103
+ resetLapTelemetry(car, currentTime, track, totalLaps);
104
+ return;
105
+ }
106
+
107
+ const sectorLength = getSectorLength(track);
108
+ if (travelled > 1e-6) {
109
+ const firstBoundary = Math.floor(previousDistance / sectorLength) + 1;
110
+ const lastBoundary = Math.floor(currentDistance / sectorLength);
111
+
112
+ for (let boundary = firstBoundary; boundary <= lastBoundary; boundary += 1) {
113
+ const boundaryDistance = boundary * sectorLength;
114
+ if (boundaryDistance <= previousDistance + 1e-3 || boundaryDistance > currentDistance + 1e-3) continue;
115
+
116
+ const boundaryRatio = clamp((boundaryDistance - previousDistance) / travelled, 0, 1);
117
+ const crossingTime = previousUpdateTime + boundaryRatio * elapsedTime;
118
+ const sectorIndex = (boundary - 1) % TELEMETRY_SECTOR_COUNT;
119
+ const sectorTime = Math.max(0, crossingTime - telemetry.currentSectorStartedAt);
120
+
121
+ telemetry.currentSectors[sectorIndex] = sectorTime;
122
+ updateBestSector(telemetry, sectorIndex, sectorTime);
123
+
124
+ if (sectorIndex === TELEMETRY_SECTOR_COUNT - 1) {
125
+ const lapTime = Math.max(0, crossingTime - telemetry.currentLapStartedAt);
126
+ telemetry.lastLapTime = lapTime;
127
+ telemetry.bestLapTime = Number.isFinite(telemetry.bestLapTime)
128
+ ? Math.min(telemetry.bestLapTime, lapTime)
129
+ : lapTime;
130
+ telemetry.lastSectors = serializeSectorTimes(telemetry.currentSectors);
131
+ telemetry.currentSectors = createEmptySectorTimes();
132
+ telemetry.currentLapStartedAt = crossingTime;
133
+ telemetry.completedLaps = Math.max(telemetry.completedLaps + 1, Math.min(Math.floor(boundaryDistance / track.length), totalLaps));
134
+ }
135
+
136
+ telemetry.currentSectorStartedAt = crossingTime;
137
+ }
138
+ }
139
+
140
+ syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps);
141
+ telemetry.lastUpdatedAt = currentTime;
142
+ }
143
+
144
+ function updateBestSector(telemetry, sectorIndex, sectorTime) {
145
+ const previousBest = telemetry.bestSectors[sectorIndex];
146
+ if (!Number.isFinite(previousBest) || sectorTime < previousBest) {
147
+ telemetry.bestSectors[sectorIndex] = sectorTime;
148
+ }
149
+ }
150
+
151
+ function clearFutureSectorTelemetry(telemetry) {
152
+ const activeIndex = clamp((telemetry.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
153
+ for (let index = activeIndex; index < TELEMETRY_SECTOR_COUNT; index += 1) {
154
+ telemetry.currentSectors[index] = null;
155
+ }
156
+ }
157
+
158
+ function syncLiveSectorTelemetry(telemetry) {
159
+ const activeIndex = clamp((telemetry.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
160
+
161
+ telemetry.liveSectors = createEmptySectorTimes();
162
+ telemetry.currentSectors.forEach((time, index) => {
163
+ if (index >= activeIndex) return;
164
+ if (!Number.isFinite(time)) return;
165
+ telemetry.liveSectors[index] = time;
166
+ });
167
+ telemetry.liveSectors[activeIndex] = telemetry.currentSectorElapsed;
168
+ }
169
+
170
+ function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps) {
171
+ const position = getLapTelemetryPosition(track, currentRaceDistance, totalLaps);
172
+ telemetry.completedLaps = position.completedLaps;
173
+ telemetry.currentLap = position.currentLap;
174
+ telemetry.currentSector = position.currentSector;
175
+ telemetry.currentSectorProgress = position.currentSectorProgress;
176
+ clearFutureSectorTelemetry(telemetry);
177
+ telemetry.sectorProgress = createSectorProgress(position, telemetry.currentSectors);
178
+ telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(position.completedLaps, totalLaps));
179
+ telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
180
+ telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
181
+ syncLiveSectorTelemetry(telemetry);
182
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ createLapTelemetry,
3
+ resetLapTelemetry,
4
+ serializeLapTelemetry,
5
+ updateLapTelemetry,
6
+ } from './lapTelemetry.js';
7
+ export { estimateGapAheadSeconds, estimateTimingLineGapSeconds, wholeLapGap } from './gapEstimation.js';
8
+ export { createEmptySectorPerformance, createEmptySectorTimes, updateSectorPerformance } from './sectorPerformance.js';
9
+ export { recordTimingSample, resetTimingHistory, trimTimingHistory } from './timingHistory.js';
10
+ export {
11
+ createTimingLines,
12
+ getTimingLineNumber,
13
+ recordTimingLineCrossings,
14
+ resetTimingLineCrossings,
15
+ } from './timingLines.js';
@@ -0,0 +1,60 @@
1
+ import { TELEMETRY_SECTOR_COUNT } from './timingConstants.js';
2
+
3
+ export function createEmptySectorTimes() {
4
+ return Array.from({ length: TELEMETRY_SECTOR_COUNT }, () => null);
5
+ }
6
+
7
+ export function createEmptySectorPerformance() {
8
+ return {
9
+ current: createEmptySectorTimes(),
10
+ last: createEmptySectorTimes(),
11
+ best: createEmptySectorTimes(),
12
+ };
13
+ }
14
+
15
+ export function serializeSectorTimes(values) {
16
+ return createEmptySectorTimes().map((_, index) => finiteOrNull(values?.[index]));
17
+ }
18
+
19
+ export function serializeSectorPerformance(values) {
20
+ return createEmptySectorTimes().map((_, index) => values?.[index] ?? null);
21
+ }
22
+
23
+ export function updateSectorPerformance(cars) {
24
+ const overallBestSectors = createEmptySectorTimes();
25
+ cars.forEach((car) => {
26
+ car.lapTelemetry?.bestSectors?.forEach((value, index) => {
27
+ if (!Number.isFinite(value)) return;
28
+ const previousBest = overallBestSectors[index];
29
+ if (!Number.isFinite(previousBest) || value < previousBest) overallBestSectors[index] = value;
30
+ });
31
+ });
32
+ cars.forEach((car) => {
33
+ const telemetry = car.lapTelemetry;
34
+ if (!telemetry) return;
35
+ telemetry.sectorPerformance = {
36
+ current: telemetry.currentSectors.map((value, index) => classifySectorPerformance(
37
+ value,
38
+ telemetry.bestSectors[index],
39
+ overallBestSectors[index],
40
+ )),
41
+ last: telemetry.lastSectors.map((value, index) => classifySectorPerformance(
42
+ value,
43
+ telemetry.bestSectors[index],
44
+ overallBestSectors[index],
45
+ )),
46
+ best: telemetry.bestSectors.map((value, index) => classifySectorPerformance(value, value, overallBestSectors[index])),
47
+ };
48
+ });
49
+ }
50
+
51
+ export function finiteOrNull(value) {
52
+ return Number.isFinite(value) ? value : null;
53
+ }
54
+
55
+ function classifySectorPerformance(value, personalBest, overallBest) {
56
+ if (!Number.isFinite(value)) return null;
57
+ if (Number.isFinite(overallBest) && Math.abs(value - overallBest) <= 1e-6) return 'overall-best';
58
+ if (Number.isFinite(personalBest) && Math.abs(value - personalBest) <= 1e-6) return 'personal-best';
59
+ return 'slower';
60
+ }
@@ -0,0 +1,5 @@
1
+ export const TELEMETRY_SECTOR_COUNT = 3;
2
+ export const TIMING_HISTORY_WINDOW_SECONDS = 18;
3
+ export const TIMING_HISTORY_MAX_SAMPLES = 720;
4
+ export const TIMING_LINE_TARGET_SPACING_METERS = 175;
5
+ export const TIMING_LINE_HISTORY_LAPS = 3;
@@ -0,0 +1,46 @@
1
+ import { TIMING_HISTORY_MAX_SAMPLES, TIMING_HISTORY_WINDOW_SECONDS } from './timingConstants.js';
2
+
3
+ export function trimTimingHistory(history, currentTime) {
4
+ const cutoff = currentTime - TIMING_HISTORY_WINDOW_SECONDS;
5
+ while (history.length > 2 && (history[0].time < cutoff || history.length > TIMING_HISTORY_MAX_SAMPLES)) {
6
+ history.shift();
7
+ }
8
+ }
9
+
10
+ export function resetTimingHistory(car, currentTime) {
11
+ car.timingHistory = [{ time: currentTime, raceDistance: car.raceDistance }];
12
+ }
13
+
14
+ export function recordTimingSample(car, currentTime) {
15
+ if (!Number.isFinite(car.raceDistance)) return;
16
+ if (!Array.isArray(car.timingHistory)) {
17
+ resetTimingHistory(car, currentTime);
18
+ return;
19
+ }
20
+
21
+ const previous = car.timingHistory[car.timingHistory.length - 1];
22
+ if (
23
+ previous &&
24
+ Math.abs(previous.time - currentTime) <= 1e-6 &&
25
+ Math.abs(previous.raceDistance - car.raceDistance) <= 1e-3
26
+ ) {
27
+ return;
28
+ }
29
+
30
+ car.timingHistory.push({ time: currentTime, raceDistance: car.raceDistance });
31
+ trimTimingHistory(car.timingHistory, currentTime);
32
+ }
33
+
34
+ export function interpolateTimeAtDistance(history, targetDistance) {
35
+ if (!Array.isArray(history) || history.length < 2) return null;
36
+ for (let index = history.length - 1; index > 0; index -= 1) {
37
+ const current = history[index];
38
+ const previous = history[index - 1];
39
+ if (targetDistance < previous.raceDistance || targetDistance > current.raceDistance) continue;
40
+ const coveredDistance = current.raceDistance - previous.raceDistance;
41
+ if (coveredDistance <= 1e-6) return current.time;
42
+ const ratio = (targetDistance - previous.raceDistance) / coveredDistance;
43
+ return previous.time + (current.time - previous.time) * ratio;
44
+ }
45
+ return null;
46
+ }
@@ -0,0 +1,79 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { metersToSimUnits, simUnitsToMeters } from '../units.js';
3
+ import { pointAt } from '../track/trackModel.js';
4
+ import { TIMING_LINE_HISTORY_LAPS, TIMING_LINE_TARGET_SPACING_METERS } from './timingConstants.js';
5
+
6
+ export function createTimingLines(track) {
7
+ const targetSpacing = metersToSimUnits(TIMING_LINE_TARGET_SPACING_METERS);
8
+ const count = Math.max(1, Math.round(track.length / targetSpacing));
9
+ const spacing = track.length / count;
10
+ return {
11
+ spacing,
12
+ spacingMeters: simUnitsToMeters(spacing),
13
+ count,
14
+ lines: Array.from({ length: count }, (_, index) => {
15
+ const distance = index * spacing;
16
+ const point = pointAt(track, distance);
17
+ return {
18
+ index,
19
+ distance,
20
+ distanceMeters: simUnitsToMeters(distance),
21
+ x: point.x,
22
+ y: point.y,
23
+ heading: point.heading,
24
+ };
25
+ }),
26
+ };
27
+ }
28
+
29
+ export function getTimingLineNumber(track, raceDistance) {
30
+ const spacing = track.timingLines?.spacing;
31
+ if (!Number.isFinite(spacing) || spacing <= 0 || !Number.isFinite(raceDistance)) return null;
32
+ return Math.floor(Math.max(0, raceDistance) / spacing);
33
+ }
34
+
35
+ export function resetTimingLineCrossings(car, currentTime) {
36
+ car.timingLineCrossings = Object.create(null);
37
+ car.timingLineLastUpdatedAt = currentTime;
38
+ car.previousRaceDistanceForTiming = car.raceDistance;
39
+ }
40
+
41
+ export function recordTimingLineCrossings(car, previousRaceDistance, currentTime, track) {
42
+ const spacing = track.timingLines?.spacing;
43
+ if (!Number.isFinite(spacing) || spacing <= 0 || !Number.isFinite(car.raceDistance)) return;
44
+ if (!car.timingLineCrossings || typeof car.timingLineCrossings !== 'object') {
45
+ resetTimingLineCrossings(car, currentTime);
46
+ return;
47
+ }
48
+
49
+ const previousDistance = Number.isFinite(previousRaceDistance) ? previousRaceDistance : car.raceDistance;
50
+ const currentDistance = car.raceDistance;
51
+ const travelled = currentDistance - previousDistance;
52
+ const previousTime = Number.isFinite(car.timingLineLastUpdatedAt) ? car.timingLineLastUpdatedAt : currentTime;
53
+ const elapsed = Math.max(0, currentTime - previousTime);
54
+
55
+ if (travelled > 1e-6 && travelled < track.length / 2) {
56
+ const firstLine = Math.floor(Math.max(0, previousDistance) / spacing) + 1;
57
+ const lastLine = Math.floor(Math.max(0, currentDistance) / spacing);
58
+ for (let lineNumber = firstLine; lineNumber <= lastLine; lineNumber += 1) {
59
+ const lineDistance = lineNumber * spacing;
60
+ if (lineDistance <= previousDistance + 1e-6 || lineDistance > currentDistance + 1e-6) continue;
61
+ const ratio = clamp((lineDistance - previousDistance) / travelled, 0, 1);
62
+ car.timingLineCrossings[lineNumber] = previousTime + ratio * elapsed;
63
+ }
64
+ }
65
+
66
+ car.timingLineLastUpdatedAt = currentTime;
67
+ const latestLine = getTimingLineNumber(track, currentDistance);
68
+ if (latestLine != null) {
69
+ const cutoff = latestLine - (track.timingLines.count * TIMING_LINE_HISTORY_LAPS);
70
+ Object.keys(car.timingLineCrossings).forEach((key) => {
71
+ if (Number(key) < cutoff) delete car.timingLineCrossings[key];
72
+ });
73
+ }
74
+ }
75
+
76
+ export function getTimingLineCrossingTime(car, lineNumber) {
77
+ const value = car?.timingLineCrossings?.[lineNumber];
78
+ return Number.isFinite(value) ? value : null;
79
+ }
@@ -0,0 +1,59 @@
1
+ import { distance } from './trackMath.js';
2
+ import { CENTERLINE_CONTROLS } from './trackConstants.js';
3
+
4
+ export function uniformCatmullRom(p0, p1, p2, p3, t) {
5
+ const t2 = t * t;
6
+ const t3 = t2 * t;
7
+ return {
8
+ x: 0.5 * (
9
+ (2 * p1.x) +
10
+ (-p0.x + p2.x) * t +
11
+ (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
12
+ (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3
13
+ ),
14
+ y: 0.5 * (
15
+ (2 * p1.y) +
16
+ (-p0.y + p2.y) * t +
17
+ (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
18
+ (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3
19
+ ),
20
+ };
21
+ }
22
+
23
+ export function centripetalCatmullRom(p0, p1, p2, p3, t) {
24
+ const t0 = 0;
25
+ const t1 = t0 + Math.sqrt(Math.max(distance(p0, p1), 0.001));
26
+ const t2 = t1 + Math.sqrt(Math.max(distance(p1, p2), 0.001));
27
+ const t3 = t2 + Math.sqrt(Math.max(distance(p2, p3), 0.001));
28
+ const localT = t1 + (t2 - t1) * t;
29
+
30
+ const interpolate = (a, b, start, end) => {
31
+ const span = Math.max(end - start, 0.001);
32
+ const amount = (localT - start) / span;
33
+ return {
34
+ x: a.x + (b.x - a.x) * amount,
35
+ y: a.y + (b.y - a.y) * amount,
36
+ };
37
+ };
38
+
39
+ const a1 = interpolate(p0, p1, t0, t1);
40
+ const a2 = interpolate(p1, p2, t1, t2);
41
+ const a3 = interpolate(p2, p3, t2, t3);
42
+ const b1 = interpolate(a1, a2, t0, t2);
43
+ const b2 = interpolate(a2, a3, t1, t3);
44
+ return interpolate(b1, b2, t1, t2);
45
+ }
46
+
47
+ export function rawCenterPoint(ratio, controls = CENTERLINE_CONTROLS, interpolation = 'uniform') {
48
+ const count = controls.length;
49
+ const scaled = ratio * count;
50
+ const index = Math.floor(scaled) % count;
51
+ const localT = scaled - Math.floor(scaled);
52
+ const p0 = controls[(index - 1 + count) % count];
53
+ const p1 = controls[index];
54
+ const p2 = controls[(index + 1) % count];
55
+ const p3 = controls[(index + 2) % count];
56
+ return interpolation === 'centripetal'
57
+ ? centripetalCatmullRom(p0, p1, p2, p3, localT)
58
+ : uniformCatmullRom(p0, p1, p2, p3, localT);
59
+ }
@@ -0,0 +1,67 @@
1
+ import { wrapDistance } from '../simMath.js';
2
+
3
+ export function normalizeDrsZone(zone, totalLength) {
4
+ if (Number.isFinite(zone.start) && Number.isFinite(zone.end)) return zone;
5
+ const start = zone.startRatio * totalLength;
6
+ const rawEnd = zone.endRatio * totalLength;
7
+ return {
8
+ ...zone,
9
+ start,
10
+ end: rawEnd >= start ? rawEnd : rawEnd + totalLength,
11
+ };
12
+ }
13
+
14
+ export function scoreStraightWindow(samples, startIndex, windowSize) {
15
+ let curvature = 0;
16
+ for (let offset = 0; offset < windowSize; offset += 1) {
17
+ curvature += samples[(startIndex + offset) % (samples.length - 1)].curvature;
18
+ }
19
+ return curvature / windowSize;
20
+ }
21
+
22
+ export function deriveDrsZones(samples, totalLength) {
23
+ const usableSampleCount = samples.length - 1;
24
+ const windowSize = Math.max(96, Math.floor(usableSampleCount * 0.07));
25
+ const candidates = [];
26
+
27
+ for (let index = 0; index < usableSampleCount; index += Math.floor(windowSize / 2)) {
28
+ const start = samples[index];
29
+ const end = samples[(index + windowSize) % usableSampleCount];
30
+ const distance = end.distance >= start.distance
31
+ ? end.distance - start.distance
32
+ : totalLength - start.distance + end.distance;
33
+ if (distance < 360) continue;
34
+ candidates.push({
35
+ startRatio: start.distance / totalLength,
36
+ endRatio: (start.distance + Math.min(distance, totalLength * 0.16)) / totalLength,
37
+ score: scoreStraightWindow(samples, index, windowSize),
38
+ });
39
+ }
40
+
41
+ return candidates
42
+ .sort((a, b) => a.score - b.score)
43
+ .reduce((zones, candidate) => {
44
+ const farEnough = zones.every((zone) => Math.abs(zone.startRatio - candidate.startRatio) > 0.18);
45
+ if (farEnough && zones.length < 3) {
46
+ zones.push({
47
+ id: `generated-drs-${zones.length + 1}`,
48
+ startRatio: candidate.startRatio % 1,
49
+ endRatio: candidate.endRatio % 1,
50
+ });
51
+ }
52
+ return zones;
53
+ }, [])
54
+ .sort((a, b) => a.startRatio - b.startRatio);
55
+ }
56
+
57
+ export function isInDrsZone(track, progress) {
58
+ const wrapped = wrapDistance(progress, track.length);
59
+ return track.drsZones.some((zone) => {
60
+ const start = wrapDistance(zone.start, track.length);
61
+ const end = wrapDistance(zone.end, track.length);
62
+ if (zone.end - zone.start >= track.length) return true;
63
+ return end >= start
64
+ ? wrapped >= start && wrapped <= end
65
+ : wrapped >= start || wrapped <= end;
66
+ });
67
+ }
@@ -0,0 +1,103 @@
1
+ import { clamp, wrapDistance } from '../simMath.js';
2
+ import { PIT_ACCESS_MAX_LENGTH, PIT_ACCESS_SAMPLE_STEPS, PIT_ACCESS_SEARCH_STEP, PIT_ACCESS_TRACK_OVERLAP, PIT_LANE_WIDTH } from './trackConstants.js';
3
+ import { angleBetweenVectors, headingVector, normalizeVector, sampleCubicBezier, signedLateralOffsetToPoint } from './trackMath.js';
4
+ import { offsetTrackPoint, pointAt } from './spatialQueries.js';
5
+
6
+ export function createPitAccessRoadCenterline(start, end, startForward, endForward, tangentLength) {
7
+ return sampleCubicBezier(
8
+ start,
9
+ {
10
+ x: start.x + startForward.x * tangentLength,
11
+ y: start.y + startForward.y * tangentLength,
12
+ },
13
+ {
14
+ x: end.x - endForward.x * tangentLength,
15
+ y: end.y - endForward.y * tangentLength,
16
+ },
17
+ end,
18
+ PIT_ACCESS_SAMPLE_STEPS,
19
+ );
20
+ }
21
+
22
+ export function createLaneFacingTrackConnection(track, distanceFromStart, lanePoint) {
23
+ const trackPoint = pointAt(track, distanceFromStart);
24
+ const laneSide = signedLateralOffsetToPoint(trackPoint, lanePoint) >= 0 ? 1 : -1;
25
+ const edgePoint = offsetTrackPoint(trackPoint, laneSide * (track.width / 2));
26
+ const trackConnectPoint = offsetTrackPoint(
27
+ trackPoint,
28
+ laneSide * Math.max(0, track.width / 2 - PIT_ACCESS_TRACK_OVERLAP),
29
+ );
30
+
31
+ return {
32
+ distanceFromStart,
33
+ trackDistance: wrapDistance(trackPoint.distance, track.length),
34
+ trackPoint,
35
+ edgePoint,
36
+ trackConnectPoint,
37
+ laneSide,
38
+ };
39
+ }
40
+
41
+ export function scorePitAccessConnection(track, connection, lanePoint, laneForward, direction) {
42
+ const roadVector = direction === 'entry'
43
+ ? normalizeVector({
44
+ x: lanePoint.x - connection.trackConnectPoint.x,
45
+ y: lanePoint.y - connection.trackConnectPoint.y,
46
+ })
47
+ : normalizeVector({
48
+ x: connection.trackConnectPoint.x - lanePoint.x,
49
+ y: connection.trackConnectPoint.y - lanePoint.y,
50
+ });
51
+ const trackForward = headingVector(connection.trackPoint.heading);
52
+ const trackAngle = angleBetweenVectors(roadVector, trackForward);
53
+ const laneAngle = angleBetweenVectors(roadVector, laneForward);
54
+ const laneFacingClearance = Math.abs(signedLateralOffsetToPoint(connection.trackPoint, lanePoint)) - track.width / 2;
55
+
56
+ return {
57
+ ...connection,
58
+ roadVector,
59
+ trackAngle,
60
+ laneAngle,
61
+ pathLength: roadVector.length,
62
+ score:
63
+ trackAngle * 1100 +
64
+ laneAngle * 900 +
65
+ roadVector.length * 0.32 -
66
+ Math.max(0, laneFacingClearance) * 0.18,
67
+ };
68
+ }
69
+
70
+ export function findPitAccessConnection(track, lanePoint, laneForward, {
71
+ direction,
72
+ startDistance,
73
+ endDistance,
74
+ fallbackDistance,
75
+ }) {
76
+ const start = Math.min(startDistance, endDistance);
77
+ const end = Math.max(startDistance, endDistance);
78
+ let best = null;
79
+ let fallback = null;
80
+
81
+ for (let distanceFromStart = start; distanceFromStart <= end; distanceFromStart += PIT_ACCESS_SEARCH_STEP) {
82
+ const connection = scorePitAccessConnection(
83
+ track,
84
+ createLaneFacingTrackConnection(track, distanceFromStart, lanePoint),
85
+ lanePoint,
86
+ laneForward,
87
+ direction,
88
+ );
89
+ fallback = !fallback || connection.score < fallback.score ? connection : fallback;
90
+ if (connection.trackAngle > 1.15 || connection.laneAngle > 1.15) continue;
91
+ best = !best || connection.score < best.score ? connection : best;
92
+ }
93
+
94
+ if (best) return best;
95
+
96
+ return fallback ?? scorePitAccessConnection(
97
+ track,
98
+ createLaneFacingTrackConnection(track, fallbackDistance, lanePoint),
99
+ lanePoint,
100
+ laneForward,
101
+ direction,
102
+ );
103
+ }