@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.
- package/CHANGELOG.md +176 -1
- package/INSTALL_AND_UPDATE.md +24 -4
- package/README.md +84 -512
- package/docs/architecture.md +38 -24
- package/docs/component_inventory.md +72 -0
- package/docs/composable-layouts.md +77 -0
- package/docs/concepts.md +2 -2
- package/docs/custom_model_controller.md +6 -6
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +104 -27
- package/docs/getting-started.md +75 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +18 -4
- package/docs/learnings.md +6 -6
- package/docs/rules.md +12 -8
- package/docs/sense_contract.md +1 -1
- package/docs/system_specs.md +71 -21
- package/docs/theming.md +64 -0
- package/docs/training.md +26 -19
- package/docs/troubleshooting.md +76 -0
- package/docs/upgrading-hosts-to-3.md +42 -0
- package/examples/python/README.md +6 -0
- package/examples/python/base_policy_server.py +19 -0
- package/package.json +10 -3
- package/src/api/PaddockSimulatorController.js +150 -24
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +213 -29
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +262 -49
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +58 -38
- package/src/config/themeOptions.js +830 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/controllerLoop.js +17 -3
- package/src/environment/episode.js +11 -1
- package/src/environment/index.d.ts +1 -2
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/options.js +1 -1
- package/src/environment/runtime.js +44 -11
- package/src/environment/scenarios.js +1 -1
- package/src/environment/sensors/carRays.js +48 -37
- package/src/environment/sensors/index.js +501 -97
- package/src/environment/sensors/indexedRayBands.js +134 -46
- package/src/environment/sensors/rayBandTrace.js +1171 -0
- package/src/environment/sensors/rayChannels.js +108 -0
- package/src/environment/sensors/rayConfig.js +71 -25
- package/src/environment/sensors/rayGeometry.js +23 -17
- package/src/environment/sensors/sensorTargets.js +67 -1
- package/src/environment/sensors/surfaceRays.js +167 -118
- package/src/environment/sensors/trackRays.js +157 -48
- package/src/index.d.ts +153 -9
- package/src/index.js +52 -3
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitKinematics.js +2 -2
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +79 -46
- package/src/simulation/pit/pitState.js +1 -0
- package/src/simulation/pit/pitTireService.js +7 -3
- package/src/simulation/race/raceFinish.js +25 -8
- package/src/simulation/race/raceOrder.js +189 -32
- package/src/simulation/race/raceProgress.js +129 -37
- package/src/simulation/race/raceSetup.js +4 -6
- package/src/simulation/race/raceStep.js +26 -14
- package/src/simulation/race/safetyCar.js +1 -1
- package/src/simulation/race/stalledDnf.js +1 -1
- package/src/simulation/raceSimulation.js +20 -17
- package/src/simulation/rules/collisionSteward.js +40 -23
- package/src/simulation/rules/rulesReview.js +77 -56
- package/src/simulation/snapshots/raceSnapshots.js +37 -0
- package/src/simulation/timing/drsTiming.js +143 -30
- package/src/simulation/timing/gapEstimation.js +25 -3
- package/src/simulation/timing/lapTelemetry.js +138 -40
- package/src/simulation/timing/raceTiming.js +5 -1
- package/src/simulation/timing/sectorPerformance.js +104 -26
- package/src/simulation/timing/timingHistory.js +229 -14
- package/src/simulation/timing/timingLines.js +65 -7
- package/src/simulation/track/pitLaneLayout.js +30 -3
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +66 -33
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1468 -189
- package/src/simulation/track/trackQueryPitIndex.js +138 -14
- package/src/simulation/track/trackQueryScratch.js +7 -0
- package/src/simulation/track/trackQueryStats.js +25 -0
- package/src/simulation/track/trackSnapshotJson.js +139 -0
- package/src/simulation/track/trackStatePolicy.js +249 -2
- package/src/simulation/vehicle/contactResolution.js +99 -62
- package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
- package/src/simulation/vehicle/pitWheelSurface.js +81 -28
- package/src/simulation/vehicle/runoffResponse.js +54 -29
- package/src/simulation/vehicle/vehicleGeometry.js +245 -51
- package/src/simulation/vehicle/vehicleKinematics.js +2 -2
- package/src/simulation/vehicle/vehiclePhysics.js +45 -30
- package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
- package/src/simulation/vehicle/vehicleState.js +7 -1
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1568 -519
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- 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
|
-
|
|
6
|
-
|
|
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 =
|
|
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 (!
|
|
139
|
+
if (!isStructuredTimingHistory(car.timingHistory)) {
|
|
17
140
|
resetTimingHistory(car, currentTime);
|
|
18
|
-
return;
|
|
19
141
|
}
|
|
20
|
-
|
|
21
|
-
const
|
|
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
|
-
|
|
24
|
-
Math.abs(
|
|
25
|
-
Math.abs(
|
|
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
|
-
|
|
31
|
-
|
|
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 (
|
|
36
|
-
|
|
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
|
|
123
|
+
setTimingLineCrossing(car, lineNumber, previousTime + ratio * elapsed);
|
|
63
124
|
}
|
|
64
125
|
}
|
|
65
126
|
|
|
66
127
|
car.timingLineLastUpdatedAt = currentTime;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|