@inventure71/paddockjs 2.0.1 → 4.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 (136) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +84 -512
  4. package/docs/architecture.md +38 -24
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/composable-layouts.md +77 -0
  7. package/docs/concepts.md +2 -2
  8. package/docs/custom_model_controller.md +6 -6
  9. package/docs/data-helpers.md +50 -0
  10. package/docs/data_contract.md +104 -27
  11. package/docs/getting-started.md +75 -0
  12. package/docs/headless-environment.md +68 -0
  13. package/docs/index.md +18 -4
  14. package/docs/learnings.md +6 -6
  15. package/docs/rules.md +12 -8
  16. package/docs/sense_contract.md +1 -1
  17. package/docs/system_specs.md +71 -21
  18. package/docs/theming.md +64 -0
  19. package/docs/training.md +26 -19
  20. package/docs/troubleshooting.md +76 -0
  21. package/docs/upgrading-hosts-to-3.md +42 -0
  22. package/examples/python/README.md +6 -0
  23. package/examples/python/base_policy_server.py +19 -0
  24. package/package.json +10 -3
  25. package/src/api/PaddockSimulatorController.js +150 -24
  26. package/src/api/runtimeTheme.js +59 -0
  27. package/src/app/F1SimulatorApp.js +213 -29
  28. package/src/app/banners/raceDataBannerController.js +71 -36
  29. package/src/app/banners/stewardMessageController.js +5 -4
  30. package/src/app/camera/cameraConstants.js +2 -0
  31. package/src/app/camera/cameraController.js +86 -29
  32. package/src/app/camera/cameraGeometry.js +12 -1
  33. package/src/app/domBindings.js +89 -9
  34. package/src/app/layoutScheduler.js +27 -0
  35. package/src/app/layoutSupport.js +165 -0
  36. package/src/app/raceOverlayClearanceSupport.js +158 -0
  37. package/src/app/readouts/carOverviewRenderer.js +16 -8
  38. package/src/app/readouts/controlStateRenderer.js +1 -1
  39. package/src/app/readouts/raceStatusRenderer.js +30 -21
  40. package/src/app/readouts/readoutFormatters.js +2 -0
  41. package/src/app/readouts/telemetryRenderer.js +17 -8
  42. package/src/app/readouts/timingTowerRenderer.js +262 -49
  43. package/src/app/runtime/frameLoop.js +5 -1
  44. package/src/app/runtime/runtimeVisibility.js +66 -5
  45. package/src/config/cssValues.js +16 -0
  46. package/src/config/defaultOptions.js +58 -38
  47. package/src/config/themeOptions.js +830 -0
  48. package/src/config/timingGapMode.js +13 -0
  49. package/src/config/urlOptions.js +25 -0
  50. package/src/data/championship.js +14 -9
  51. package/src/data/index.d.ts +212 -0
  52. package/src/data/index.js +14 -0
  53. package/src/environment/controllerLoop.js +17 -3
  54. package/src/environment/episode.js +11 -1
  55. package/src/environment/index.d.ts +1 -2
  56. package/src/environment/metrics.js +59 -16
  57. package/src/environment/observationVector.js +50 -33
  58. package/src/environment/observations.js +89 -20
  59. package/src/environment/options.js +1 -1
  60. package/src/environment/runtime.js +44 -11
  61. package/src/environment/scenarios.js +1 -1
  62. package/src/environment/sensors/carRays.js +48 -37
  63. package/src/environment/sensors/index.js +501 -97
  64. package/src/environment/sensors/indexedRayBands.js +134 -46
  65. package/src/environment/sensors/rayBandTrace.js +1171 -0
  66. package/src/environment/sensors/rayChannels.js +108 -0
  67. package/src/environment/sensors/rayConfig.js +71 -25
  68. package/src/environment/sensors/rayGeometry.js +23 -17
  69. package/src/environment/sensors/sensorTargets.js +67 -1
  70. package/src/environment/sensors/surfaceRays.js +167 -118
  71. package/src/environment/sensors/trackRays.js +157 -48
  72. package/src/index.d.ts +153 -9
  73. package/src/index.js +52 -3
  74. package/src/simulation/collisionGeometry.js +260 -96
  75. package/src/simulation/driver/driverController.js +1 -1
  76. package/src/simulation/driver/edgeRecovery.js +1 -1
  77. package/src/simulation/driver/racingControls.js +2 -2
  78. package/src/simulation/driver/rejoinControls.js +2 -2
  79. package/src/simulation/pit/pitFlow.js +21 -9
  80. package/src/simulation/pit/pitKinematics.js +2 -2
  81. package/src/simulation/pit/pitQueue.js +5 -3
  82. package/src/simulation/pit/pitRouteMovement.js +9 -4
  83. package/src/simulation/pit/pitRouting.js +79 -46
  84. package/src/simulation/pit/pitState.js +1 -0
  85. package/src/simulation/pit/pitTireService.js +7 -3
  86. package/src/simulation/race/raceFinish.js +25 -8
  87. package/src/simulation/race/raceOrder.js +189 -32
  88. package/src/simulation/race/raceProgress.js +129 -37
  89. package/src/simulation/race/raceSetup.js +4 -6
  90. package/src/simulation/race/raceStep.js +26 -14
  91. package/src/simulation/race/safetyCar.js +1 -1
  92. package/src/simulation/race/stalledDnf.js +1 -1
  93. package/src/simulation/raceSimulation.js +20 -17
  94. package/src/simulation/rules/collisionSteward.js +40 -23
  95. package/src/simulation/rules/rulesReview.js +77 -56
  96. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  97. package/src/simulation/timing/drsTiming.js +143 -30
  98. package/src/simulation/timing/gapEstimation.js +25 -3
  99. package/src/simulation/timing/lapTelemetry.js +138 -40
  100. package/src/simulation/timing/raceTiming.js +5 -1
  101. package/src/simulation/timing/sectorPerformance.js +104 -26
  102. package/src/simulation/timing/timingHistory.js +229 -14
  103. package/src/simulation/timing/timingLines.js +65 -7
  104. package/src/simulation/track/pitLaneLayout.js +30 -3
  105. package/src/simulation/track/pitLaneState.js +218 -19
  106. package/src/simulation/track/spatialQueries.js +66 -33
  107. package/src/simulation/track/trackMath.js +91 -24
  108. package/src/simulation/track/trackModel.js +2 -0
  109. package/src/simulation/track/trackQueryIndex.js +1468 -189
  110. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  111. package/src/simulation/track/trackQueryScratch.js +7 -0
  112. package/src/simulation/track/trackQueryStats.js +25 -0
  113. package/src/simulation/track/trackSnapshotJson.js +139 -0
  114. package/src/simulation/track/trackStatePolicy.js +249 -2
  115. package/src/simulation/vehicle/contactResolution.js +99 -62
  116. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  117. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  118. package/src/simulation/vehicle/runoffResponse.js +54 -29
  119. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  120. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  121. package/src/simulation/vehicle/vehiclePhysics.js +45 -30
  122. package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
  123. package/src/simulation/vehicle/vehicleState.js +7 -1
  124. package/src/simulation/vehicle/wheelSurface.js +807 -92
  125. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  126. package/src/styles.css +1568 -519
  127. package/src/ui/bannerTemplates.js +38 -28
  128. package/src/ui/cameraControlsTemplate.js +16 -5
  129. package/src/ui/carOverviewTemplate.js +12 -5
  130. package/src/ui/raceCanvasTemplate.js +31 -8
  131. package/src/ui/raceControlsTemplate.js +27 -10
  132. package/src/ui/shellTemplate.js +3 -1
  133. package/src/ui/telemetryDrawerTemplate.js +23 -10
  134. package/src/ui/telemetryTemplates.js +58 -32
  135. package/src/ui/templateUtils.js +48 -0
  136. package/src/ui/timingTowerTemplate.js +29 -12
@@ -1,39 +1,218 @@
1
1
  import { TIMING_HISTORY_MAX_SAMPLES, TIMING_HISTORY_WINDOW_SECONDS } from './timingConstants.js';
2
2
 
3
+ const TIMING_HISTORY_START_INDEX = '_startIndex';
4
+ const TIMING_HISTORY_COUNT = '_count';
5
+ const TIMING_HISTORY_INTERPOLATION_OFFSET = '_interpolationOffset';
6
+ const TIMING_HISTORY_INTERPOLATION_LOWER_DISTANCE = '_interpolationLowerDistance';
7
+ const TIMING_HISTORY_INTERPOLATION_UPPER_DISTANCE = '_interpolationUpperDistance';
8
+ const TIMING_HISTORY_INTERPOLATION_LOWER_TIME = '_interpolationLowerTime';
9
+ const TIMING_HISTORY_INTERPOLATION_UPPER_TIME = '_interpolationUpperTime';
10
+ const TIMING_HISTORY_INTERPOLATION_STATS = '_interpolationStats';
11
+
12
+ function isStructuredTimingHistory(history) {
13
+ return Boolean(
14
+ history &&
15
+ history.times instanceof Float64Array &&
16
+ history.distances instanceof Float64Array,
17
+ );
18
+ }
19
+
20
+ function createStructuredTimingHistory(currentTime, raceDistance) {
21
+ const history = {
22
+ times: new Float64Array(TIMING_HISTORY_MAX_SAMPLES),
23
+ distances: new Float64Array(TIMING_HISTORY_MAX_SAMPLES),
24
+ };
25
+ history.times[0] = currentTime;
26
+ history.distances[0] = raceDistance;
27
+ history[TIMING_HISTORY_START_INDEX] = 0;
28
+ history[TIMING_HISTORY_COUNT] = 1;
29
+ return history;
30
+ }
31
+
32
+ function getTimingHistoryStartIndex(history) {
33
+ if (isStructuredTimingHistory(history)) {
34
+ const value = history[TIMING_HISTORY_START_INDEX];
35
+ return Number.isInteger(value) && value >= 0 ? value : 0;
36
+ }
37
+ const value = history?.[TIMING_HISTORY_START_INDEX];
38
+ return Number.isInteger(value) && value >= 0 ? value : 0;
39
+ }
40
+
41
+ function setTimingHistoryStartIndex(history, startIndex) {
42
+ if (isStructuredTimingHistory(history)) {
43
+ history[TIMING_HISTORY_START_INDEX] = startIndex;
44
+ return;
45
+ }
46
+ Object.defineProperty(history, TIMING_HISTORY_START_INDEX, {
47
+ value: startIndex,
48
+ writable: true,
49
+ configurable: true,
50
+ enumerable: false,
51
+ });
52
+ }
53
+
54
+ function clearTimingHistoryStartIndex(history) {
55
+ if (isStructuredTimingHistory(history)) {
56
+ history[TIMING_HISTORY_START_INDEX] = 0;
57
+ return;
58
+ }
59
+ delete history[TIMING_HISTORY_START_INDEX];
60
+ }
61
+
62
+ function activeTimingHistoryLength(history, startIndex = getTimingHistoryStartIndex(history)) {
63
+ if (isStructuredTimingHistory(history)) {
64
+ const count = history[TIMING_HISTORY_COUNT];
65
+ return Number.isInteger(count) && count >= 0 ? count : 0;
66
+ }
67
+ return Math.max(0, history.length - startIndex);
68
+ }
69
+
70
+ function compactTimingHistory(history, startIndex) {
71
+ if (isStructuredTimingHistory(history)) {
72
+ history[TIMING_HISTORY_START_INDEX] = startIndex;
73
+ return;
74
+ }
75
+ if (startIndex <= 0) {
76
+ clearTimingHistoryStartIndex(history);
77
+ return;
78
+ }
79
+ history.splice(0, startIndex);
80
+ clearTimingHistoryStartIndex(history);
81
+ }
82
+
83
+ function trimStructuredTimingHistory(history, currentTime) {
84
+ let startIndex = getTimingHistoryStartIndex(history);
85
+ let count = activeTimingHistoryLength(history, startIndex);
86
+ const cutoff = currentTime - TIMING_HISTORY_WINDOW_SECONDS;
87
+ while (
88
+ count > 2 &&
89
+ history.times[startIndex] < cutoff
90
+ ) {
91
+ startIndex = (startIndex + 1) % TIMING_HISTORY_MAX_SAMPLES;
92
+ count -= 1;
93
+ }
94
+ history[TIMING_HISTORY_START_INDEX] = startIndex;
95
+ history[TIMING_HISTORY_COUNT] = count;
96
+ }
97
+
3
98
  export function trimTimingHistory(history, currentTime) {
99
+ if (isStructuredTimingHistory(history)) {
100
+ trimStructuredTimingHistory(history, currentTime);
101
+ return;
102
+ }
103
+ if (!Array.isArray(history)) return;
4
104
  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();
105
+ let startIndex = getTimingHistoryStartIndex(history);
106
+ if (
107
+ startIndex === 0 &&
108
+ history.length <= TIMING_HISTORY_MAX_SAMPLES &&
109
+ history.length <= 2
110
+ ) return;
111
+ if (
112
+ startIndex === 0 &&
113
+ history.length <= TIMING_HISTORY_MAX_SAMPLES &&
114
+ history[0]?.time >= cutoff
115
+ ) return;
116
+ while (
117
+ activeTimingHistoryLength(history, startIndex) > 2 &&
118
+ (
119
+ history[startIndex]?.time < cutoff ||
120
+ activeTimingHistoryLength(history, startIndex) > TIMING_HISTORY_MAX_SAMPLES
121
+ )
122
+ ) {
123
+ startIndex += 1;
7
124
  }
125
+ if (startIndex > 0 && (startIndex >= 64 || startIndex * 2 >= history.length)) {
126
+ compactTimingHistory(history, startIndex);
127
+ return;
128
+ }
129
+ if (startIndex > 0) setTimingHistoryStartIndex(history, startIndex);
130
+ else clearTimingHistoryStartIndex(history);
8
131
  }
9
132
 
10
133
  export function resetTimingHistory(car, currentTime) {
11
- car.timingHistory = [{ time: currentTime, raceDistance: car.raceDistance }];
134
+ car.timingHistory = createStructuredTimingHistory(currentTime, car.raceDistance);
12
135
  }
13
136
 
14
137
  export function recordTimingSample(car, currentTime) {
15
138
  if (!Number.isFinite(car.raceDistance)) return;
16
- if (!Array.isArray(car.timingHistory)) {
139
+ if (!isStructuredTimingHistory(car.timingHistory)) {
17
140
  resetTimingHistory(car, currentTime);
18
- return;
19
141
  }
20
-
21
- const previous = car.timingHistory[car.timingHistory.length - 1];
142
+ const history = car.timingHistory;
143
+ const startIndex = getTimingHistoryStartIndex(history);
144
+ const count = activeTimingHistoryLength(history, startIndex);
145
+ const previousIndex = count > 0
146
+ ? (startIndex + count - 1) % TIMING_HISTORY_MAX_SAMPLES
147
+ : 0;
148
+ const previousTime = count > 0 ? history.times[previousIndex] : null;
149
+ const previousRaceDistance = count > 0 ? history.distances[previousIndex] : null;
22
150
  if (
23
- previous &&
24
- Math.abs(previous.time - currentTime) <= 1e-6 &&
25
- Math.abs(previous.raceDistance - car.raceDistance) <= 1e-3
151
+ count > 0 &&
152
+ Math.abs(previousTime - currentTime) <= 1e-6 &&
153
+ Math.abs(previousRaceDistance - car.raceDistance) <= 1e-3
26
154
  ) {
27
155
  return;
28
156
  }
29
157
 
30
- car.timingHistory.push({ time: currentTime, raceDistance: car.raceDistance });
31
- trimTimingHistory(car.timingHistory, currentTime);
158
+ let nextStartIndex = startIndex;
159
+ let nextCount = count;
160
+ let writeIndex = 0;
161
+ if (nextCount < TIMING_HISTORY_MAX_SAMPLES) {
162
+ writeIndex = (nextStartIndex + nextCount) % TIMING_HISTORY_MAX_SAMPLES;
163
+ nextCount += 1;
164
+ } else {
165
+ writeIndex = nextStartIndex;
166
+ nextStartIndex = (nextStartIndex + 1) % TIMING_HISTORY_MAX_SAMPLES;
167
+ }
168
+ history.times[writeIndex] = currentTime;
169
+ history.distances[writeIndex] = car.raceDistance;
170
+ history[TIMING_HISTORY_START_INDEX] = nextStartIndex;
171
+ history[TIMING_HISTORY_COUNT] = nextCount;
172
+ trimStructuredTimingHistory(history, currentTime);
32
173
  }
33
174
 
34
175
  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) {
176
+ if (isStructuredTimingHistory(history)) {
177
+ const directCachedTime = interpolateDirectCachedSegment(history, targetDistance);
178
+ if (Number.isFinite(directCachedTime)) return directCachedTime;
179
+ const startIndex = getTimingHistoryStartIndex(history);
180
+ const count = activeTimingHistoryLength(history, startIndex);
181
+ if (count < 2) return null;
182
+ const cachedOffset = history[TIMING_HISTORY_INTERPOLATION_OFFSET];
183
+ if (Number.isInteger(cachedOffset) && cachedOffset >= 0 && cachedOffset < count - 1) {
184
+ const cachedTime = interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, cachedOffset);
185
+ if (Number.isFinite(cachedTime)) return cachedTime;
186
+ const resolvedOffset = history[TIMING_HISTORY_INTERPOLATION_OFFSET];
187
+ if (Number.isInteger(resolvedOffset) && resolvedOffset >= 0 && resolvedOffset < count - 1) {
188
+ const currentPosition = count - 1 - resolvedOffset;
189
+ const currentIndex = (startIndex + currentPosition) % TIMING_HISTORY_MAX_SAMPLES;
190
+ const previousIndex = (startIndex + currentPosition - 1) % TIMING_HISTORY_MAX_SAMPLES;
191
+ const currentDistance = history.distances[currentIndex];
192
+ const previousDistance = history.distances[previousIndex];
193
+ if (targetDistance > currentDistance) {
194
+ for (let offset = resolvedOffset - 1; offset >= 0; offset -= 1) {
195
+ const value = interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, offset);
196
+ if (Number.isFinite(value)) return value;
197
+ }
198
+ } else if (targetDistance < previousDistance) {
199
+ for (let offset = resolvedOffset + 1; offset < count - 1; offset += 1) {
200
+ const value = interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, offset);
201
+ if (Number.isFinite(value)) return value;
202
+ }
203
+ }
204
+ }
205
+ }
206
+ for (let offset = 0; offset < count - 1; offset += 1) {
207
+ const value = interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, offset);
208
+ if (Number.isFinite(value)) return value;
209
+ }
210
+ return null;
211
+ }
212
+ if (!Array.isArray(history)) return null;
213
+ const startIndex = getTimingHistoryStartIndex(history);
214
+ if (activeTimingHistoryLength(history, startIndex) < 2) return null;
215
+ for (let index = history.length - 1; index > startIndex; index -= 1) {
37
216
  const current = history[index];
38
217
  const previous = history[index - 1];
39
218
  if (targetDistance < previous.raceDistance || targetDistance > current.raceDistance) continue;
@@ -44,3 +223,39 @@ export function interpolateTimeAtDistance(history, targetDistance) {
44
223
  }
45
224
  return null;
46
225
  }
226
+
227
+ function interpolateStructuredTimingHistoryAtOffset(history, startIndex, count, targetDistance, offsetFromTail) {
228
+ const currentPosition = count - 1 - offsetFromTail;
229
+ const currentIndex = (startIndex + currentPosition) % TIMING_HISTORY_MAX_SAMPLES;
230
+ const previousIndex = (startIndex + currentPosition - 1) % TIMING_HISTORY_MAX_SAMPLES;
231
+ const currentDistance = history.distances[currentIndex];
232
+ const previousDistance = history.distances[previousIndex];
233
+ if (targetDistance < previousDistance || targetDistance > currentDistance) return null;
234
+ history[TIMING_HISTORY_INTERPOLATION_OFFSET] = offsetFromTail;
235
+ history[TIMING_HISTORY_INTERPOLATION_LOWER_DISTANCE] = previousDistance;
236
+ history[TIMING_HISTORY_INTERPOLATION_UPPER_DISTANCE] = currentDistance;
237
+ history[TIMING_HISTORY_INTERPOLATION_LOWER_TIME] = history.times[previousIndex];
238
+ history[TIMING_HISTORY_INTERPOLATION_UPPER_TIME] = history.times[currentIndex];
239
+ const coveredDistance = currentDistance - previousDistance;
240
+ if (coveredDistance <= 1e-6) return history.times[currentIndex];
241
+ const ratio = (targetDistance - previousDistance) / coveredDistance;
242
+ return history.times[previousIndex] + (history.times[currentIndex] - history.times[previousIndex]) * ratio;
243
+ }
244
+
245
+ function interpolateDirectCachedSegment(history, targetDistance) {
246
+ const lowerDistance = history[TIMING_HISTORY_INTERPOLATION_LOWER_DISTANCE];
247
+ const upperDistance = history[TIMING_HISTORY_INTERPOLATION_UPPER_DISTANCE];
248
+ if (!Number.isFinite(lowerDistance) || !Number.isFinite(upperDistance)) return null;
249
+ if (targetDistance < lowerDistance || targetDistance > upperDistance) return null;
250
+
251
+ const lowerTime = history[TIMING_HISTORY_INTERPOLATION_LOWER_TIME];
252
+ const upperTime = history[TIMING_HISTORY_INTERPOLATION_UPPER_TIME];
253
+ const stats = history[TIMING_HISTORY_INTERPOLATION_STATS];
254
+ if (stats) {
255
+ stats.directSegmentCacheHits = (stats.directSegmentCacheHits ?? 0) + 1;
256
+ }
257
+ const coveredDistance = upperDistance - lowerDistance;
258
+ if (coveredDistance <= 1e-6) return upperTime;
259
+ const ratio = (targetDistance - lowerDistance) / coveredDistance;
260
+ return lowerTime + (upperTime - lowerTime) * ratio;
261
+ }
@@ -3,6 +3,53 @@ import { metersToSimUnits, simUnitsToMeters } from '../units.js';
3
3
  import { pointAt } from '../track/trackModel.js';
4
4
  import { TIMING_LINE_HISTORY_LAPS, TIMING_LINE_TARGET_SPACING_METERS } from './timingConstants.js';
5
5
 
6
+ function setTimingLineCrossingBounds(car, firstLine, lastLine) {
7
+ car.timingLineFirstStored = Number.isInteger(firstLine) ? firstLine : null;
8
+ car.timingLineLastStored = Number.isInteger(lastLine) ? lastLine : null;
9
+ }
10
+
11
+ function ensureTimingLineCrossingBounds(car) {
12
+ if (Number.isInteger(car.timingLineFirstStored) && Number.isInteger(car.timingLineLastStored)) {
13
+ return {
14
+ firstLine: car.timingLineFirstStored,
15
+ lastLine: car.timingLineLastStored,
16
+ };
17
+ }
18
+ const lineNumbers = Object.keys(car.timingLineCrossings)
19
+ .map((key) => Number(key))
20
+ .filter((value) => Number.isFinite(value))
21
+ .sort((left, right) => left - right);
22
+ const firstLine = lineNumbers[0];
23
+ const lastLine = lineNumbers[lineNumbers.length - 1];
24
+ setTimingLineCrossingBounds(car, firstLine, lastLine);
25
+ return {
26
+ firstLine: car.timingLineFirstStored,
27
+ lastLine: car.timingLineLastStored,
28
+ };
29
+ }
30
+
31
+ function setTimingLineCrossing(car, lineNumber, crossingTime) {
32
+ car.timingLineCrossings[lineNumber] = crossingTime;
33
+ const firstLine = car.timingLineFirstStored;
34
+ const lastLine = car.timingLineLastStored;
35
+ if (!Number.isInteger(firstLine) || lineNumber < firstLine) car.timingLineFirstStored = lineNumber;
36
+ if (!Number.isInteger(lastLine) || lineNumber > lastLine) car.timingLineLastStored = lineNumber;
37
+ }
38
+
39
+ function pruneTimingLineCrossings(car, cutoffLine) {
40
+ const { firstLine, lastLine } = ensureTimingLineCrossingBounds(car);
41
+ if (!Number.isInteger(firstLine) || !Number.isInteger(lastLine) || cutoffLine <= firstLine) return;
42
+ const pruneEnd = Math.min(lastLine, cutoffLine - 1);
43
+ for (let lineNumber = firstLine; lineNumber <= pruneEnd; lineNumber += 1) {
44
+ delete car.timingLineCrossings[lineNumber];
45
+ }
46
+ if (cutoffLine > lastLine) {
47
+ setTimingLineCrossingBounds(car, null, null);
48
+ return;
49
+ }
50
+ setTimingLineCrossingBounds(car, cutoffLine, lastLine);
51
+ }
52
+
6
53
  export function createTimingLines(track) {
7
54
  const targetSpacing = metersToSimUnits(TIMING_LINE_TARGET_SPACING_METERS);
8
55
  const count = Math.max(1, Math.round(track.length / targetSpacing));
@@ -36,6 +83,8 @@ export function resetTimingLineCrossings(car, currentTime) {
36
83
  car.timingLineCrossings = Object.create(null);
37
84
  car.timingLineLastUpdatedAt = currentTime;
38
85
  car.previousRaceDistanceForTiming = car.raceDistance;
86
+ setTimingLineCrossingBounds(car, null, null);
87
+ car.timingLinePruneCutoff = null;
39
88
  }
40
89
 
41
90
  export function recordTimingLineCrossings(car, previousRaceDistance, currentTime, track) {
@@ -51,6 +100,18 @@ export function recordTimingLineCrossings(car, previousRaceDistance, currentTime
51
100
  const travelled = currentDistance - previousDistance;
52
101
  const previousTime = Number.isFinite(car.timingLineLastUpdatedAt) ? car.timingLineLastUpdatedAt : currentTime;
53
102
  const elapsed = Math.max(0, currentTime - previousTime);
103
+ const latestLine = getTimingLineNumber(track, currentDistance);
104
+ const cutoff = latestLine != null
105
+ ? latestLine - (track.timingLines.count * TIMING_LINE_HISTORY_LAPS)
106
+ : null;
107
+
108
+ if (Math.abs(travelled) <= 1e-6) {
109
+ car.timingLineLastUpdatedAt = currentTime;
110
+ if (!Number.isFinite(cutoff) || cutoff <= 0 || cutoff === car.timingLinePruneCutoff) return;
111
+ pruneTimingLineCrossings(car, cutoff);
112
+ car.timingLinePruneCutoff = cutoff;
113
+ return;
114
+ }
54
115
 
55
116
  if (travelled > 1e-6 && travelled < track.length / 2) {
56
117
  const firstLine = Math.floor(Math.max(0, previousDistance) / spacing) + 1;
@@ -59,17 +120,14 @@ export function recordTimingLineCrossings(car, previousRaceDistance, currentTime
59
120
  const lineDistance = lineNumber * spacing;
60
121
  if (lineDistance <= previousDistance + 1e-6 || lineDistance > currentDistance + 1e-6) continue;
61
122
  const ratio = clamp((lineDistance - previousDistance) / travelled, 0, 1);
62
- car.timingLineCrossings[lineNumber] = previousTime + ratio * elapsed;
123
+ setTimingLineCrossing(car, lineNumber, previousTime + ratio * elapsed);
63
124
  }
64
125
  }
65
126
 
66
127
  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
- });
128
+ if (Number.isFinite(cutoff) && cutoff > 0 && cutoff !== car.timingLinePruneCutoff) {
129
+ pruneTimingLineCrossings(car, cutoff);
130
+ car.timingLinePruneCutoff = cutoff;
73
131
  }
74
132
  }
75
133
 
@@ -1,9 +1,9 @@
1
1
  import { clamp } from '../simMath.js';
2
2
  import { metersToSimUnits } from '../units.js';
3
3
  import { WORLD, PIT_BOX_COUNT, PIT_BOX_DEPTH, PIT_BOX_LENGTH, PIT_BOX_PAIR_GAP, PIT_BOX_TO_LANE_GAP, PIT_ENTRY_SEARCH_AFTER, PIT_ENTRY_SEARCH_BEFORE, PIT_EXIT_SEARCH_AFTER, PIT_EXIT_SEARCH_BEFORE, PIT_LANE_EDGE_GAP, PIT_LANE_ENTRY_BUFFER, PIT_LANE_EXIT_BUFFER, PIT_LANE_FINISH_RATIO, PIT_LANE_OFFSET_SEARCH_STEP, PIT_LANE_WIDTH, PIT_SERVICE_AREA_DEPTH, PIT_SERVICE_AREA_LENGTH, PIT_SERVICE_QUEUE_GAP, PIT_TEAM_COUNT, PIT_TEAM_GAP, PIT_BOXES_PER_TEAM, PIT_WORKING_LANE_GAP, PIT_WORKING_LANE_WIDTH, PIT_ACCESS_MAX_LENGTH, PIT_ACCESS_MIN_LENGTH, PIT_ACCESS_TANGENT_RATIO, PIT_TRACK_CLEARANCE_MARGIN } from './trackConstants.js';
4
- import { clonePoint, distance, headingVector, interpolatePitPoint, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
4
+ import { clonePoint, distance, headingVector, interpolatePitPoint, nearestPointOnPolyline, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
5
5
  import { findPitAccessConnection, createPitAccessRoadCenterline } from './pitLaneAccess.js';
6
- import { nearestTrackState, pointAt } from './spatialQueries.js';
6
+ import { pointAt } from './spatialQueries.js';
7
7
 
8
8
  export function createStartStraightBasis(track) {
9
9
  const finish = pointAt(track, 0);
@@ -90,7 +90,7 @@ export function scorePitLanePlacement(track, side, laneOffset, layout) {
90
90
  minimumClearance = Math.min(minimumClearance, pointWorldClearance(lanePoint), pointWorldClearance(boxPoint));
91
91
  minimumTrackClearance = Math.min(
92
92
  minimumTrackClearance,
93
- nearestTrackState(track, lanePoint).crossTrackError -
93
+ nearestMainTrackProjection(track, lanePoint).crossTrackError -
94
94
  (track.width / 2 + (track.kerbWidth ?? 0) + PIT_TRACK_CLEARANCE_MARGIN),
95
95
  );
96
96
  }
@@ -105,6 +105,10 @@ export function scorePitLanePlacement(track, side, laneOffset, layout) {
105
105
  };
106
106
  }
107
107
 
108
+ function nearestMainTrackProjection(track, position) {
109
+ return nearestPointOnPolyline(track.samples, position) ?? { crossTrackError: Infinity };
110
+ }
111
+
108
112
  export function choosePitLanePlacement(track, baseLaneOffset, layout) {
109
113
  const candidates = [];
110
114
  for (let offsetStep = 0; offsetStep <= 9; offsetStep += 1) {
@@ -270,6 +274,27 @@ export function createPitLaneBounds(pitLane) {
270
274
  return createPointBounds(points, padding);
271
275
  }
272
276
 
277
+ export function createPitBoxBounds(pitLane) {
278
+ const points = [
279
+ ...(pitLane.boxes ?? []).flatMap((box) => box.corners ?? []),
280
+ ...(pitLane.serviceAreas ?? []).flatMap((area) => [
281
+ ...(area.corners ?? []),
282
+ ...(area.queueCorners ?? []),
283
+ ]),
284
+ ];
285
+ if (!points.length) return null;
286
+ const padding = Math.max(PIT_BOX_DEPTH, PIT_SERVICE_AREA_DEPTH) / 2 + metersToSimUnits(3);
287
+ return createPointBounds(points, padding);
288
+ }
289
+
290
+ export function createPitConnectorBounds(pitLane) {
291
+ const connectorPadding = metersToSimUnits(18);
292
+ return {
293
+ entry: createPointBounds(pitLane.entry?.connector ?? [], connectorPadding),
294
+ exit: createPointBounds(pitLane.exit?.connector ?? [], connectorPadding),
295
+ };
296
+ }
297
+
273
298
  export function createPitLaneModel(track) {
274
299
  const layout = createPitLaneLayout();
275
300
  const laneOffset = track.width / 2 + (track.kerbWidth ?? 0) + PIT_LANE_EDGE_GAP + PIT_LANE_WIDTH / 2;
@@ -391,5 +416,7 @@ export function createPitLaneModel(track) {
391
416
  return {
392
417
  ...pitLane,
393
418
  bounds: createPitLaneBounds(pitLane),
419
+ boxBounds: createPitBoxBounds(pitLane),
420
+ connectorBounds: createPitConnectorBounds(pitLane),
394
421
  };
395
422
  }