@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,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getNextTimingGapMode, getTimingGapModeLabel, normalizeTimingGapMode } from '../../config/timingGapMode.js';
|
|
2
|
+
import { setText, setTextAll } from '../domBindings.js';
|
|
3
|
+
import {
|
|
4
|
+
escapeHtml,
|
|
5
|
+
formatCssColor,
|
|
6
|
+
formatLapGap,
|
|
7
|
+
formatRaceGap,
|
|
8
|
+
getTireClass,
|
|
9
|
+
} from './readoutFormatters.js';
|
|
3
10
|
|
|
4
11
|
function formatPenaltyHeadline(penalty) {
|
|
5
12
|
const serviceType = penalty?.serviceType;
|
|
@@ -104,12 +111,206 @@ export function getTimingOrderKey(cars = [], { timingGapMode = 'interval' } = {}
|
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
export function syncTimingGapModeControls({ readouts, buttons, timingGapMode }) {
|
|
107
|
-
const
|
|
108
|
-
|
|
114
|
+
const normalizedMode = normalizeTimingGapMode(timingGapMode);
|
|
115
|
+
const label = getTimingGapModeLabel(normalizedMode);
|
|
116
|
+
const nextLabel = getTimingGapModeLabel(getNextTimingGapMode(normalizedMode));
|
|
117
|
+
setTextAll(readouts.timingGapLabels ?? readouts.timingGapLabel, label);
|
|
109
118
|
buttons.forEach((button) => {
|
|
110
|
-
const
|
|
111
|
-
button
|
|
112
|
-
button.
|
|
119
|
+
const leaderMode = normalizedMode === 'leader';
|
|
120
|
+
setText(button, label);
|
|
121
|
+
button.setAttribute('aria-pressed', String(leaderMode));
|
|
122
|
+
button.setAttribute('aria-label', `Timing gap mode ${label}; switch to ${nextLabel}`);
|
|
123
|
+
button.setAttribute('title', `Switch to ${nextLabel}`);
|
|
124
|
+
button.classList?.toggle?.('is-active', leaderMode);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function timingGapForCar(car, raceMode, timingGapMode) {
|
|
129
|
+
if (isDnfCar(car)) return 'DNF';
|
|
130
|
+
if (isWavedFlagCar(car) && raceMode !== 'finished') return 'WAVED';
|
|
131
|
+
if (raceMode === 'finished') return car.rank === 1 ? 'Winner' : 'FIN';
|
|
132
|
+
if (raceMode === 'safety-car' && car.rank > 1) return 'SC';
|
|
133
|
+
if (raceMode === 'pre-start') return car.rank === 1 ? 'Pole' : 'Grid';
|
|
134
|
+
if (car.rank > 1) return formatRaceGap(car, timingGapMode);
|
|
135
|
+
return 'Leader';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function timingRowModel({ car, driver, raceMode, selectedId, timingGapMode, penalty }) {
|
|
139
|
+
const dnf = isDnfCar(car);
|
|
140
|
+
const tire = car.tire ?? driver?.tire ?? 'M';
|
|
141
|
+
const timingCode = car.timingCode ?? driver?.timingCode ?? car.code;
|
|
142
|
+
const team = car.team ?? driver?.team ?? null;
|
|
143
|
+
const icon = team?.icon ?? car.icon ?? driver?.icon ?? timingCode;
|
|
144
|
+
const driverColor = formatCssColor(car.color);
|
|
145
|
+
const iconColor = formatCssColor(team?.color ?? car.color, driverColor);
|
|
146
|
+
const penaltyLabel = penalty ? formatPenaltyBadgeLabel(penalty) : '';
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
id: car.id,
|
|
150
|
+
rank: car.rank,
|
|
151
|
+
name: car.name,
|
|
152
|
+
selected: car.id === selectedId,
|
|
153
|
+
dnf,
|
|
154
|
+
gap: timingGapForCar(car, raceMode, timingGapMode),
|
|
155
|
+
tire,
|
|
156
|
+
tireClass: getTireClass(tire),
|
|
157
|
+
timingCode,
|
|
158
|
+
icon,
|
|
159
|
+
driverColor,
|
|
160
|
+
iconColor,
|
|
161
|
+
penaltyLabel,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function timingRowKey(row) {
|
|
166
|
+
return [
|
|
167
|
+
row.id,
|
|
168
|
+
row.rank,
|
|
169
|
+
row.name,
|
|
170
|
+
row.selected ? 1 : 0,
|
|
171
|
+
row.dnf ? 1 : 0,
|
|
172
|
+
row.gap,
|
|
173
|
+
row.tire,
|
|
174
|
+
row.tireClass,
|
|
175
|
+
row.timingCode,
|
|
176
|
+
row.icon,
|
|
177
|
+
row.driverColor,
|
|
178
|
+
row.iconColor,
|
|
179
|
+
row.penaltyLabel,
|
|
180
|
+
].join('\u001f');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function timingRowsKey(rows) {
|
|
184
|
+
return rows.map(timingRowKey).join('\u001e');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderTimingRowMarkup(row) {
|
|
188
|
+
const penaltyBadge = row.penaltyLabel
|
|
189
|
+
? `<span class="timing-penalty-badge" aria-label="${escapeHtml(row.penaltyLabel)}" title="${escapeHtml(row.penaltyLabel)}">!</span>`
|
|
190
|
+
: '';
|
|
191
|
+
|
|
192
|
+
return `
|
|
193
|
+
<li>
|
|
194
|
+
<button class="timing-row ${row.selected ? 'is-selected' : ''} ${row.dnf ? 'is-dnf' : ''}" type="button"
|
|
195
|
+
data-driver-id="${escapeHtml(row.id)}" aria-label="Select ${escapeHtml(row.name)}"
|
|
196
|
+
style="--driver-color: ${escapeHtml(row.driverColor)}">
|
|
197
|
+
<span class="timing-position">${row.rank}</span>
|
|
198
|
+
<span class="timing-icon timing-team-icon" aria-hidden="true" style="--team-color: ${escapeHtml(row.iconColor)}">${escapeHtml(row.icon)}</span>
|
|
199
|
+
<span class="timing-name" title="${escapeHtml(row.name)}"><span>${escapeHtml(row.timingCode)}</span>${penaltyBadge}</span>
|
|
200
|
+
<span class="timing-gap">${escapeHtml(row.gap)}</span>
|
|
201
|
+
<span class="timing-tire timing-tire--${row.tireClass}">${escapeHtml(row.tire)}</span>
|
|
202
|
+
</button>
|
|
203
|
+
</li>
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function canPatchTimingRows(timingList) {
|
|
208
|
+
return Boolean(
|
|
209
|
+
timingList?.ownerDocument?.createElement &&
|
|
210
|
+
typeof timingList.appendChild === 'function' &&
|
|
211
|
+
timingList.children,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function createTimingRowView(document) {
|
|
216
|
+
const item = document.createElement('li');
|
|
217
|
+
const button = document.createElement('button');
|
|
218
|
+
const position = document.createElement('span');
|
|
219
|
+
const icon = document.createElement('span');
|
|
220
|
+
const name = document.createElement('span');
|
|
221
|
+
const code = document.createElement('span');
|
|
222
|
+
const gap = document.createElement('span');
|
|
223
|
+
const tire = document.createElement('span');
|
|
224
|
+
|
|
225
|
+
item.appendChild(button);
|
|
226
|
+
button.appendChild(position);
|
|
227
|
+
button.appendChild(icon);
|
|
228
|
+
button.appendChild(name);
|
|
229
|
+
name.appendChild(code);
|
|
230
|
+
button.appendChild(gap);
|
|
231
|
+
button.appendChild(tire);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
item,
|
|
235
|
+
button,
|
|
236
|
+
position,
|
|
237
|
+
icon,
|
|
238
|
+
name,
|
|
239
|
+
code,
|
|
240
|
+
gap,
|
|
241
|
+
tire,
|
|
242
|
+
penaltyBadge: null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function setStyleProperty(element, name, value) {
|
|
247
|
+
if (typeof element.style?.setProperty === 'function') {
|
|
248
|
+
element.style.setProperty(name, value);
|
|
249
|
+
} else {
|
|
250
|
+
element.setAttribute?.('style', `${name}: ${value}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function updateTimingRowView(view, row) {
|
|
255
|
+
view.button.className = `timing-row ${row.selected ? 'is-selected' : ''} ${row.dnf ? 'is-dnf' : ''}`;
|
|
256
|
+
view.button.setAttribute('type', 'button');
|
|
257
|
+
view.button.setAttribute('data-driver-id', row.id);
|
|
258
|
+
view.button.setAttribute('aria-label', `Select ${row.name}`);
|
|
259
|
+
setStyleProperty(view.button, '--driver-color', row.driverColor);
|
|
260
|
+
|
|
261
|
+
view.position.className = 'timing-position';
|
|
262
|
+
view.position.textContent = String(row.rank);
|
|
263
|
+
|
|
264
|
+
view.icon.className = 'timing-icon timing-team-icon';
|
|
265
|
+
view.icon.setAttribute('aria-hidden', 'true');
|
|
266
|
+
setStyleProperty(view.icon, '--team-color', row.iconColor);
|
|
267
|
+
view.icon.textContent = String(row.icon ?? '');
|
|
268
|
+
|
|
269
|
+
view.name.className = 'timing-name';
|
|
270
|
+
view.name.setAttribute('title', row.name);
|
|
271
|
+
view.code.textContent = String(row.timingCode ?? '');
|
|
272
|
+
|
|
273
|
+
if (row.penaltyLabel) {
|
|
274
|
+
if (!view.penaltyBadge) {
|
|
275
|
+
view.penaltyBadge = view.name.ownerDocument.createElement('span');
|
|
276
|
+
view.name.appendChild(view.penaltyBadge);
|
|
277
|
+
}
|
|
278
|
+
view.penaltyBadge.className = 'timing-penalty-badge';
|
|
279
|
+
view.penaltyBadge.setAttribute('aria-label', row.penaltyLabel);
|
|
280
|
+
view.penaltyBadge.setAttribute('title', row.penaltyLabel);
|
|
281
|
+
view.penaltyBadge.textContent = '!';
|
|
282
|
+
} else if (view.penaltyBadge) {
|
|
283
|
+
view.penaltyBadge.remove();
|
|
284
|
+
view.penaltyBadge = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
view.gap.className = 'timing-gap';
|
|
288
|
+
view.gap.textContent = String(row.gap);
|
|
289
|
+
|
|
290
|
+
view.tire.className = `timing-tire timing-tire--${row.tireClass}`;
|
|
291
|
+
view.tire.textContent = String(row.tire ?? '');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function patchTimingRows(timingList, rows) {
|
|
295
|
+
const rowViews = timingList.__paddockTimingRowViews ?? new Map();
|
|
296
|
+
timingList.__paddockTimingRowViews = rowViews;
|
|
297
|
+
const seen = new Set();
|
|
298
|
+
|
|
299
|
+
rows.forEach((row) => {
|
|
300
|
+
let view = rowViews.get(row.id);
|
|
301
|
+
if (!view) {
|
|
302
|
+
view = createTimingRowView(timingList.ownerDocument);
|
|
303
|
+
rowViews.set(row.id, view);
|
|
304
|
+
}
|
|
305
|
+
seen.add(row.id);
|
|
306
|
+
updateTimingRowView(view, row);
|
|
307
|
+
timingList.appendChild(view.item);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
rowViews.forEach((view, id) => {
|
|
311
|
+
if (seen.has(id)) return;
|
|
312
|
+
view.item.remove();
|
|
313
|
+
rowViews.delete(id);
|
|
113
314
|
});
|
|
114
315
|
}
|
|
115
316
|
|
|
@@ -126,52 +327,64 @@ export function renderTimingTower({
|
|
|
126
327
|
}) {
|
|
127
328
|
if (!timingList) return lastTimingMarkup;
|
|
128
329
|
const penaltyByDriver = timingPenaltyBadgesEnabled ? getPenaltyByDriver(penalties) : new Map();
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
gap = formatRaceGap(car, timingGapMode);
|
|
145
|
-
}
|
|
146
|
-
const tire = car.tire ?? driver?.tire ?? 'M';
|
|
147
|
-
const timingCode = car.timingCode ?? driver?.timingCode ?? car.code;
|
|
148
|
-
const team = car.team ?? driver?.team ?? null;
|
|
149
|
-
const icon = team?.icon ?? car.icon ?? driver?.icon ?? timingCode;
|
|
150
|
-
const iconColor = team?.color ?? car.color;
|
|
151
|
-
const penalty = penaltyByDriver.get(car.id);
|
|
152
|
-
const penaltyBadge = penalty
|
|
153
|
-
? `<span class="timing-penalty-badge" aria-label="${escapeHtml(formatPenaltyBadgeLabel(penalty))}" title="${escapeHtml(formatPenaltyBadgeLabel(penalty))}">!</span>`
|
|
154
|
-
: '';
|
|
155
|
-
|
|
156
|
-
return `
|
|
157
|
-
<li>
|
|
158
|
-
<button class="timing-row ${car.id === selectedId ? 'is-selected' : ''} ${dnf ? 'is-dnf' : ''}" type="button"
|
|
159
|
-
data-driver-id="${escapeHtml(car.id)}" aria-label="Select ${escapeHtml(car.name)}"
|
|
160
|
-
style="--driver-color: ${escapeHtml(car.color)}">
|
|
161
|
-
<span class="timing-position">${car.rank}</span>
|
|
162
|
-
<span class="timing-icon timing-team-icon" aria-hidden="true" style="--team-color: ${escapeHtml(iconColor)}">${escapeHtml(icon)}</span>
|
|
163
|
-
<span class="timing-name" title="${escapeHtml(car.name)}"><span>${escapeHtml(timingCode)}</span>${penaltyBadge}</span>
|
|
164
|
-
<span class="timing-gap">${escapeHtml(gap)}</span>
|
|
165
|
-
<span class="timing-tire timing-tire--${getTireClass(tire)}">${escapeHtml(tire)}</span>
|
|
166
|
-
</button>
|
|
167
|
-
</li>
|
|
168
|
-
`;
|
|
169
|
-
}).join('');
|
|
170
|
-
if (timingMarkup !== lastTimingMarkup) {
|
|
330
|
+
const rows = cars.map((car) => timingRowModel({
|
|
331
|
+
car,
|
|
332
|
+
driver: driverById.get(car.id),
|
|
333
|
+
raceMode,
|
|
334
|
+
selectedId,
|
|
335
|
+
timingGapMode,
|
|
336
|
+
penalty: penaltyByDriver.get(car.id),
|
|
337
|
+
}));
|
|
338
|
+
if (canPatchTimingRows(timingList)) {
|
|
339
|
+
patchTimingRows(timingList, rows);
|
|
340
|
+
return timingRowsKey(rows);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const timingMarkup = rows.map(renderTimingRowMarkup).join('');
|
|
344
|
+
if (timingMarkup !== lastTimingMarkup || timingList.innerHTML !== timingMarkup) {
|
|
171
345
|
timingList.innerHTML = timingMarkup;
|
|
172
346
|
return timingMarkup;
|
|
173
347
|
}
|
|
174
348
|
return lastTimingMarkup;
|
|
175
349
|
}
|
|
176
350
|
|
|
351
|
+
export function renderTimingTowers({
|
|
352
|
+
timingLists,
|
|
353
|
+
timingList,
|
|
354
|
+
cars,
|
|
355
|
+
raceMode,
|
|
356
|
+
penalties = [],
|
|
357
|
+
driverById,
|
|
358
|
+
selectedId,
|
|
359
|
+
timingGapMode,
|
|
360
|
+
timingPenaltyBadgesEnabled,
|
|
361
|
+
lastTimingMarkup,
|
|
362
|
+
}) {
|
|
363
|
+
const candidateLists = typeof timingLists?.forEach === 'function'
|
|
364
|
+
? [...timingLists]
|
|
365
|
+
: [];
|
|
366
|
+
const lists = candidateLists.length > 0
|
|
367
|
+
? candidateLists
|
|
368
|
+
: timingList
|
|
369
|
+
? [timingList]
|
|
370
|
+
: [];
|
|
371
|
+
if (lists.length === 0) return lastTimingMarkup;
|
|
372
|
+
|
|
373
|
+
let nextTimingMarkup = lastTimingMarkup;
|
|
374
|
+
lists.forEach((list) => {
|
|
375
|
+
nextTimingMarkup = renderTimingTower({
|
|
376
|
+
timingList: list,
|
|
377
|
+
cars,
|
|
378
|
+
raceMode,
|
|
379
|
+
penalties,
|
|
380
|
+
driverById,
|
|
381
|
+
selectedId,
|
|
382
|
+
timingGapMode,
|
|
383
|
+
timingPenaltyBadgesEnabled,
|
|
384
|
+
lastTimingMarkup: nextTimingMarkup,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
return nextTimingMarkup;
|
|
388
|
+
}
|
|
389
|
+
|
|
177
390
|
export { formatLapGap, formatRaceGap };
|
|
@@ -48,7 +48,11 @@ export function runFrameLoopTick(app) {
|
|
|
48
48
|
const shouldUpdateDom = now - app.lastDomUpdateTime >= domUpdateIntervalForSpeed(app.simulationSpeed);
|
|
49
49
|
const fullSnapshot = stepEvents.length > 0 || shouldUpdateDom ? app.sim.snapshot() : null;
|
|
50
50
|
if (stepEvents.length > 0) app.emitRaceEvents(stepEvents, fullSnapshot);
|
|
51
|
-
const renderSource = fullSnapshot ??
|
|
51
|
+
const renderSource = fullSnapshot ??
|
|
52
|
+
app.sim.snapshotRenderInto?.(app.renderSourceSnapshotBuffer ?? {}) ??
|
|
53
|
+
app.sim.snapshotRender?.() ??
|
|
54
|
+
app.sim.snapshot();
|
|
55
|
+
if (!fullSnapshot && app.sim.snapshotRenderInto) app.renderSourceSnapshotBuffer = renderSource;
|
|
52
56
|
const renderSnapshot = interpolateRenderSnapshotInto(
|
|
53
57
|
app.renderSnapshotBuffer ?? {},
|
|
54
58
|
renderSource,
|
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
export function observeRuntimeVisibility(app) {
|
|
2
2
|
if (!app.canvasHost) return;
|
|
3
3
|
|
|
4
|
+
const syncViewportVisibility = () => {
|
|
5
|
+
app.runtimeViewportVisible = isRuntimeViewportVisible(app.canvasHost);
|
|
6
|
+
app.syncRuntimeTicker();
|
|
7
|
+
};
|
|
8
|
+
const scheduleViewportVisibilitySync = () => {
|
|
9
|
+
if (app.runtimeVisibilitySyncFrame) return;
|
|
10
|
+
if (typeof globalThis.requestAnimationFrame !== 'function') {
|
|
11
|
+
syncViewportVisibility();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
app.runtimeVisibilitySyncFrame = globalThis.requestAnimationFrame(() => {
|
|
15
|
+
app.runtimeVisibilitySyncFrame = null;
|
|
16
|
+
syncViewportVisibility();
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
4
20
|
if (typeof IntersectionObserver === 'function') {
|
|
5
21
|
app.visibilityObserver?.disconnect?.();
|
|
6
22
|
app.visibilityObserver = new IntersectionObserver((entries) => {
|
|
7
23
|
const entry = entries.find((item) => item.target === app.canvasHost) ?? entries[0];
|
|
8
|
-
app.runtimeViewportVisible = Boolean(
|
|
24
|
+
app.runtimeViewportVisible = Boolean(
|
|
25
|
+
entry?.isIntersecting ||
|
|
26
|
+
entry?.intersectionRatio > 0 ||
|
|
27
|
+
isRuntimeViewportVisible(app.canvasHost),
|
|
28
|
+
);
|
|
9
29
|
app.syncRuntimeTicker();
|
|
10
30
|
}, {
|
|
11
31
|
root: null,
|
|
@@ -25,21 +45,62 @@ export function observeRuntimeVisibility(app) {
|
|
|
25
45
|
});
|
|
26
46
|
}
|
|
27
47
|
|
|
48
|
+
syncViewportVisibility();
|
|
49
|
+
globalThis.addEventListener?.('scroll', scheduleViewportVisibilitySync, {
|
|
50
|
+
passive: true,
|
|
51
|
+
signal: app.abortController.signal,
|
|
52
|
+
});
|
|
53
|
+
globalThis.addEventListener?.('resize', scheduleViewportVisibilitySync, {
|
|
54
|
+
passive: true,
|
|
55
|
+
signal: app.abortController.signal,
|
|
56
|
+
});
|
|
57
|
+
globalThis.addEventListener?.('focus', syncViewportVisibility, {
|
|
58
|
+
signal: app.abortController.signal,
|
|
59
|
+
});
|
|
60
|
+
globalThis.addEventListener?.('pageshow', syncViewportVisibility, {
|
|
61
|
+
signal: app.abortController.signal,
|
|
62
|
+
});
|
|
28
63
|
app.syncRuntimeTicker();
|
|
29
64
|
}
|
|
30
65
|
|
|
31
66
|
export function syncRuntimeTicker(app) {
|
|
32
67
|
if (app.expertMode) return;
|
|
33
|
-
|
|
68
|
+
const ticker = app.app?.ticker;
|
|
69
|
+
if (!ticker) return;
|
|
34
70
|
|
|
35
71
|
const shouldRun = app.runtimeViewportVisible && app.runtimeDocumentVisible;
|
|
36
|
-
|
|
72
|
+
const tickerRunning = typeof ticker.started === 'boolean'
|
|
73
|
+
? ticker.started
|
|
74
|
+
: app.runtimeTickerRunning;
|
|
75
|
+
if (shouldRun === app.runtimeTickerRunning && shouldRun === tickerRunning) return;
|
|
37
76
|
|
|
38
77
|
app.runtimeTickerRunning = shouldRun;
|
|
39
78
|
if (shouldRun) {
|
|
40
79
|
app.resetFrameClock();
|
|
41
|
-
|
|
80
|
+
ticker.start?.();
|
|
42
81
|
} else {
|
|
43
|
-
|
|
82
|
+
ticker.stop?.();
|
|
44
83
|
}
|
|
45
84
|
}
|
|
85
|
+
|
|
86
|
+
function isRuntimeViewportVisible(element) {
|
|
87
|
+
const rect = element?.getBoundingClientRect?.();
|
|
88
|
+
if (!rect) return true;
|
|
89
|
+
|
|
90
|
+
const top = Number(rect.top);
|
|
91
|
+
const bottom = Number(rect.bottom);
|
|
92
|
+
const left = Number(rect.left);
|
|
93
|
+
const right = Number(rect.right);
|
|
94
|
+
const width = Number(rect.width ?? right - left);
|
|
95
|
+
const height = Number(rect.height ?? bottom - top);
|
|
96
|
+
if (![top, bottom, left, right, width, height].every(Number.isFinite)) return true;
|
|
97
|
+
if (width <= 0 || height <= 0) return false;
|
|
98
|
+
|
|
99
|
+
const viewportHeight = globalThis.innerHeight || globalThis.document?.documentElement?.clientHeight || 0;
|
|
100
|
+
const viewportWidth = globalThis.innerWidth || globalThis.document?.documentElement?.clientWidth || 0;
|
|
101
|
+
const margin = 240;
|
|
102
|
+
return bottom >= -margin &&
|
|
103
|
+
top <= viewportHeight + margin &&
|
|
104
|
+
right >= -margin &&
|
|
105
|
+
left <= viewportWidth + margin;
|
|
106
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { normalizeCssColorToken } from './themeOptions.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_SAFE_DRIVER_COLOR = '#e10600';
|
|
4
|
+
|
|
5
|
+
export function formatCssColor(value, fallback = DEFAULT_SAFE_DRIVER_COLOR) {
|
|
6
|
+
return normalizeCssColorToken(value, fallback);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatCssUrl(value) {
|
|
10
|
+
const raw = String(value ?? '');
|
|
11
|
+
const escaped = raw
|
|
12
|
+
.replaceAll('\\', '\\\\')
|
|
13
|
+
.replaceAll('"', '\\"')
|
|
14
|
+
.replace(/[\u0000-\u001F\u007F]/g, '');
|
|
15
|
+
return `url("${escaped}")`;
|
|
16
|
+
}
|
|
@@ -3,11 +3,18 @@ import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../data/championship.js';
|
|
|
3
3
|
import { normalizeSimulatorDrivers } from '../data/normalizeDrivers.js';
|
|
4
4
|
import { normalizePhysicsMode } from '../simulation/vehicle/vehiclePhysics.js';
|
|
5
5
|
import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_PADDOCK_THEME_INPUT,
|
|
8
|
+
applyPaddockThemeCssVariables,
|
|
9
|
+
mergeThemeInputs,
|
|
10
|
+
normalizePaddockTheme,
|
|
11
|
+
} from './themeOptions.js';
|
|
12
|
+
import { normalizeTimingGapMode } from './timingGapMode.js';
|
|
13
|
+
import { normalizePublicUrlOption } from './urlOptions.js';
|
|
6
14
|
|
|
7
15
|
export const DEFAULT_F1_SIMULATOR_OPTIONS = {
|
|
8
16
|
seed: 1971,
|
|
9
17
|
physicsMode: 'arcade',
|
|
10
|
-
trackQueryIndex: true,
|
|
11
18
|
warmup: {
|
|
12
19
|
enabled: true,
|
|
13
20
|
policy: 'config-change',
|
|
@@ -34,23 +41,22 @@ export const DEFAULT_F1_SIMULATOR_OPTIONS = {
|
|
|
34
41
|
},
|
|
35
42
|
showRaceDataPanel: true,
|
|
36
43
|
raceDataTelemetryDetail: false,
|
|
44
|
+
driverCamera: false,
|
|
37
45
|
raceDataBanners: {
|
|
38
46
|
initial: 'project',
|
|
39
47
|
enabled: ['project', 'radio'],
|
|
40
48
|
},
|
|
41
49
|
raceDataBannerSize: 'custom',
|
|
42
50
|
timingTowerVerticalFit: 'expand-race-view',
|
|
51
|
+
timingGapMode: 'interval',
|
|
52
|
+
timingGapModeToggle: true,
|
|
53
|
+
timingEntryVerticalPadding: 5,
|
|
54
|
+
responsiveNarrowLayout: true,
|
|
43
55
|
},
|
|
44
56
|
debug: {
|
|
45
57
|
physicsModeIndicator: false,
|
|
46
58
|
},
|
|
47
|
-
theme:
|
|
48
|
-
accentColor: '#e10600',
|
|
49
|
-
greenColor: '#14c784',
|
|
50
|
-
yellowColor: '#ffd166',
|
|
51
|
-
timingTowerMaxWidth: '390px',
|
|
52
|
-
raceViewMinHeight: '620px',
|
|
53
|
-
},
|
|
59
|
+
theme: DEFAULT_PADDOCK_THEME_INPUT,
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
export const PADDOCK_SIMULATOR_PRESETS = {
|
|
@@ -113,15 +119,21 @@ export const PADDOCK_SIMULATOR_PRESETS = {
|
|
|
113
119
|
},
|
|
114
120
|
};
|
|
115
121
|
|
|
116
|
-
export
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
export {
|
|
123
|
+
DEFAULT_PADDOCK_THEME,
|
|
124
|
+
PADDOCK_THEME_CSS_VARIABLES,
|
|
125
|
+
PADDOCK_THEME_TOKEN_KEYS,
|
|
126
|
+
applyPaddockTheme,
|
|
127
|
+
applyPaddockThemeCssVariables,
|
|
128
|
+
resolvePaddockTheme,
|
|
129
|
+
} from './themeOptions.js';
|
|
123
130
|
|
|
124
|
-
const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'show-all', 'pit']);
|
|
131
|
+
const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'driver', 'show-all', 'pit']);
|
|
132
|
+
|
|
133
|
+
function normalizeNonNegativeFiniteNumber(value, fallback) {
|
|
134
|
+
const numericValue = Number(value);
|
|
135
|
+
return Number.isFinite(numericValue) && numericValue >= 0 ? numericValue : fallback;
|
|
136
|
+
}
|
|
125
137
|
|
|
126
138
|
export function resolveF1SimulatorOptions(options = {}) {
|
|
127
139
|
const presetName = Object.hasOwn(PADDOCK_SIMULATOR_PRESETS, options.preset)
|
|
@@ -130,6 +142,9 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
130
142
|
const preset = presetName ? PADDOCK_SIMULATOR_PRESETS[presetName] : {};
|
|
131
143
|
const requestedUi = options.ui ?? {};
|
|
132
144
|
const presetUi = preset.ui ?? {};
|
|
145
|
+
const initialCameraMode = SUPPORTED_CAMERA_MODES.has(options.initialCameraMode)
|
|
146
|
+
? options.initialCameraMode
|
|
147
|
+
: DEFAULT_F1_SIMULATOR_OPTIONS.initialCameraMode;
|
|
133
148
|
const ui = {
|
|
134
149
|
...DEFAULT_F1_SIMULATOR_OPTIONS.ui,
|
|
135
150
|
...presetUi,
|
|
@@ -140,6 +155,7 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
140
155
|
...(requestedUi.raceDataBanners ?? {}),
|
|
141
156
|
},
|
|
142
157
|
};
|
|
158
|
+
ui.driverCamera = Boolean(ui.driverCamera || initialCameraMode === 'driver');
|
|
143
159
|
ui.raceDataBanners.enabled = normalizeEnabledBanners(ui.raceDataBanners.enabled);
|
|
144
160
|
ui.telemetryModules = normalizeTelemetryModules(ui.telemetryModules);
|
|
145
161
|
if (!['project', 'radio', 'hidden'].includes(ui.raceDataBanners.initial)) {
|
|
@@ -151,6 +167,13 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
151
167
|
if (!['expand-race-view', 'scroll'].includes(ui.timingTowerVerticalFit)) {
|
|
152
168
|
ui.timingTowerVerticalFit = DEFAULT_F1_SIMULATOR_OPTIONS.ui.timingTowerVerticalFit;
|
|
153
169
|
}
|
|
170
|
+
ui.timingGapMode = normalizeTimingGapMode(ui.timingGapMode);
|
|
171
|
+
ui.timingGapModeToggle = ui.timingGapModeToggle !== false;
|
|
172
|
+
ui.timingEntryVerticalPadding = normalizeNonNegativeFiniteNumber(
|
|
173
|
+
ui.timingEntryVerticalPadding,
|
|
174
|
+
DEFAULT_F1_SIMULATOR_OPTIONS.ui.timingEntryVerticalPadding,
|
|
175
|
+
);
|
|
176
|
+
ui.responsiveNarrowLayout = ui.responsiveNarrowLayout !== false;
|
|
154
177
|
if (!['auto', 'custom'].includes(ui.raceDataBannerSize)) {
|
|
155
178
|
ui.raceDataBannerSize = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBannerSize;
|
|
156
179
|
}
|
|
@@ -161,28 +184,30 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
161
184
|
...(options.debug ?? {}),
|
|
162
185
|
};
|
|
163
186
|
debug.physicsModeIndicator = Boolean(debug.physicsModeIndicator);
|
|
164
|
-
const initialCameraMode = SUPPORTED_CAMERA_MODES.has(options.initialCameraMode)
|
|
165
|
-
? options.initialCameraMode
|
|
166
|
-
: DEFAULT_F1_SIMULATOR_OPTIONS.initialCameraMode;
|
|
167
187
|
const mergedOptions = {
|
|
168
188
|
...DEFAULT_F1_SIMULATOR_OPTIONS,
|
|
169
189
|
...preset,
|
|
170
190
|
...options,
|
|
171
191
|
};
|
|
172
|
-
const theme = normalizeTheme({
|
|
173
|
-
...DEFAULT_F1_SIMULATOR_OPTIONS.theme,
|
|
174
|
-
...(preset.theme ?? {}),
|
|
175
|
-
...(options.theme ?? {}),
|
|
176
|
-
});
|
|
177
192
|
const drivers = normalizeSimulatorDrivers(mergedOptions.drivers, {
|
|
178
193
|
entries: mergedOptions.entries ?? CHAMPIONSHIP_ENTRY_BLUEPRINTS,
|
|
179
194
|
});
|
|
195
|
+
const theme = normalizePaddockTheme(mergeThemeInputs(
|
|
196
|
+
DEFAULT_F1_SIMULATOR_OPTIONS.theme,
|
|
197
|
+
preset.theme,
|
|
198
|
+
collectDriverTeamThemes(drivers),
|
|
199
|
+
options.theme,
|
|
200
|
+
));
|
|
180
201
|
|
|
181
202
|
return {
|
|
182
203
|
...DEFAULT_F1_SIMULATOR_OPTIONS,
|
|
183
204
|
...preset,
|
|
184
205
|
...options,
|
|
185
206
|
preset: presetName ?? options.preset,
|
|
207
|
+
backLinkHref: normalizePublicUrlOption(
|
|
208
|
+
mergedOptions.backLinkHref,
|
|
209
|
+
DEFAULT_F1_SIMULATOR_OPTIONS.backLinkHref,
|
|
210
|
+
),
|
|
186
211
|
physicsMode: normalizePhysicsMode(options.physicsMode ?? preset.physicsMode),
|
|
187
212
|
warmup: normalizeWarmupOptions(mergedOptions.warmup, 'browser'),
|
|
188
213
|
initialCameraMode,
|
|
@@ -194,12 +219,16 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
194
219
|
};
|
|
195
220
|
}
|
|
196
221
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
222
|
+
function collectDriverTeamThemes(drivers = []) {
|
|
223
|
+
const teamThemes = {};
|
|
224
|
+
drivers.forEach((driver) => {
|
|
225
|
+
const teamId = driver?.team?.id;
|
|
226
|
+
const theme = driver?.team?.theme;
|
|
227
|
+
if (typeof teamId === 'string' && teamId !== '' && typeof theme === 'string' && theme !== '') {
|
|
228
|
+
teamThemes[teamId] = theme;
|
|
229
|
+
}
|
|
202
230
|
});
|
|
231
|
+
return Object.keys(teamThemes).length > 0 ? { teamThemes } : {};
|
|
203
232
|
}
|
|
204
233
|
|
|
205
234
|
function normalizeEnabledBanners(value) {
|
|
@@ -226,12 +255,3 @@ function normalizeTelemetryModules(value) {
|
|
|
226
255
|
value[name] == null ? defaults[name] : Boolean(value[name]),
|
|
227
256
|
]));
|
|
228
257
|
}
|
|
229
|
-
|
|
230
|
-
function normalizeTheme(theme) {
|
|
231
|
-
return Object.fromEntries(
|
|
232
|
-
Object.entries(DEFAULT_F1_SIMULATOR_OPTIONS.theme).map(([key, fallback]) => [
|
|
233
|
-
key,
|
|
234
|
-
theme?.[key] == null || theme?.[key] === '' ? fallback : theme[key],
|
|
235
|
-
]),
|
|
236
|
-
);
|
|
237
|
-
}
|