@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,7 @@
|
|
|
1
1
|
import { Application, Container, Graphics } from 'pixi.js';
|
|
2
2
|
import { applyPaddockThemeCssVariables } from '../config/defaultOptions.js';
|
|
3
|
+
import { formatCssUrl } from '../config/cssValues.js';
|
|
4
|
+
import { getNextTimingGapMode, normalizeTimingGapMode } from '../config/timingGapMode.js';
|
|
3
5
|
import { ProceduralTrackAsset } from '../rendering/proceduralTrackAsset.js';
|
|
4
6
|
import { interpolateRenderSnapshotInto } from '../rendering/renderSnapshot.js';
|
|
5
7
|
import { createRaceSimulation, FIXED_STEP } from '../simulation/raceSimulation.js';
|
|
@@ -7,7 +9,9 @@ import { clamp } from '../simulation/simMath.js';
|
|
|
7
9
|
import { WORLD } from '../simulation/track/trackModel.js';
|
|
8
10
|
import { CameraController } from './camera/cameraController.js';
|
|
9
11
|
import { createBrowserExpertAdapter } from './BrowserExpertAdapter.js';
|
|
10
|
-
import {
|
|
12
|
+
import { installLayoutSupport } from './layoutSupport.js';
|
|
13
|
+
import { installRaceOverlayClearanceSupport } from './raceOverlayClearanceSupport.js';
|
|
14
|
+
import { querySimulatorDom, resolveNodes } from './domBindings.js';
|
|
11
15
|
import { loadAppTextures } from './rendering/appAssets.js';
|
|
12
16
|
import { CarRenderer } from './rendering/carRenderer.js';
|
|
13
17
|
import { DrsTrailRenderer } from './rendering/drsTrailRenderer.js';
|
|
@@ -18,7 +22,7 @@ import { renderTrackSurface } from './rendering/trackRenderer.js';
|
|
|
18
22
|
import {
|
|
19
23
|
getTimingOrderKey,
|
|
20
24
|
getTimingPenaltyKey,
|
|
21
|
-
|
|
25
|
+
renderTimingTowers,
|
|
22
26
|
syncTimingGapModeControls,
|
|
23
27
|
} from './readouts/timingTowerRenderer.js';
|
|
24
28
|
import { renderCarDriverOverview } from './readouts/carOverviewRenderer.js';
|
|
@@ -60,6 +64,44 @@ import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
|
|
|
60
64
|
const SIMULATION_SPEED_STEPS = [1, 2, 3, 4, 5, 10];
|
|
61
65
|
const DRS_DRAG_REDUCTION_PERCENT = 58;
|
|
62
66
|
|
|
67
|
+
function uniqueNodes(nodes = []) {
|
|
68
|
+
return [...new Set([...nodes].filter(Boolean))];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeRuntimeSnapshot(snapshot, totalLaps) {
|
|
72
|
+
const source = snapshot && typeof snapshot === 'object' ? snapshot : {};
|
|
73
|
+
const raceControlSource = source.raceControl && typeof source.raceControl === 'object'
|
|
74
|
+
? source.raceControl
|
|
75
|
+
: {};
|
|
76
|
+
const pitLaneStatus = source.pitLaneStatus ?? raceControlSource.pitLaneStatus ?? null;
|
|
77
|
+
return {
|
|
78
|
+
...source,
|
|
79
|
+
time: Number.isFinite(source.time) ? source.time : 0,
|
|
80
|
+
world: source.world ?? WORLD,
|
|
81
|
+
totalLaps: Number.isFinite(source.totalLaps) ? source.totalLaps : totalLaps,
|
|
82
|
+
raceControl: {
|
|
83
|
+
...raceControlSource,
|
|
84
|
+
mode: raceControlSource.mode ?? 'green',
|
|
85
|
+
redFlag: Boolean(raceControlSource.redFlag),
|
|
86
|
+
pitLaneOpen: Boolean(raceControlSource.pitLaneOpen),
|
|
87
|
+
pitLaneStatus,
|
|
88
|
+
finished: Boolean(raceControlSource.finished),
|
|
89
|
+
finishedAt: raceControlSource.finishedAt ?? null,
|
|
90
|
+
winner: raceControlSource.winner ?? null,
|
|
91
|
+
classification: Array.isArray(raceControlSource.classification) ? raceControlSource.classification : [],
|
|
92
|
+
start: raceControlSource.start && typeof raceControlSource.start === 'object'
|
|
93
|
+
? raceControlSource.start
|
|
94
|
+
: {},
|
|
95
|
+
},
|
|
96
|
+
pitLaneStatus,
|
|
97
|
+
safetyCar: source.safetyCar && typeof source.safetyCar === 'object' ? source.safetyCar : {},
|
|
98
|
+
events: Array.isArray(source.events) ? source.events : [],
|
|
99
|
+
penalties: Array.isArray(source.penalties) ? source.penalties : [],
|
|
100
|
+
cars: Array.isArray(source.cars) ? source.cars : [],
|
|
101
|
+
replayGhosts: Array.isArray(source.replayGhosts) ? source.replayGhosts : [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
function createMountTrackSeed() {
|
|
64
106
|
const values = new Uint32Array(1);
|
|
65
107
|
try {
|
|
@@ -105,16 +147,22 @@ export class F1SimulatorApp {
|
|
|
105
147
|
this.drivers = options.drivers;
|
|
106
148
|
this.driverById = new Map(this.drivers.map((driver) => [driver.id, driver]));
|
|
107
149
|
this.assets = options.assets;
|
|
108
|
-
this.root.style.setProperty('--broadcast-panel-surface',
|
|
150
|
+
this.root.style.setProperty('--broadcast-panel-surface', formatCssUrl(this.assets.broadcastPanel));
|
|
109
151
|
this.root.style.setProperty('--paddock-entry-count', String(this.drivers.length));
|
|
110
|
-
|
|
152
|
+
this.totalLaps = options.totalLaps;
|
|
153
|
+
this.selectedId = this.drivers[0]?.id ?? null;
|
|
154
|
+
this.lastThemeContextKey = null;
|
|
155
|
+
this.syncThemeContext();
|
|
111
156
|
Object.assign(this, querySimulatorDom(root));
|
|
112
157
|
this.abortController = new AbortController();
|
|
113
158
|
this.resizeHandler = null;
|
|
114
159
|
this.layoutResizeObserver = null;
|
|
160
|
+
this.layoutSupportCleanup = installLayoutSupport(root);
|
|
161
|
+
this.raceOverlayClearanceCleanup = installRaceOverlayClearanceSupport(root);
|
|
115
162
|
this.cameraController = null;
|
|
116
163
|
this.visibilityChangeHandler = null;
|
|
117
164
|
this.visibilityObserver = null;
|
|
165
|
+
this.runtimeVisibilitySyncFrame = null;
|
|
118
166
|
this.tickerCallback = null;
|
|
119
167
|
this.expert = null;
|
|
120
168
|
this.expertMode = Boolean(options.expert?.enabled);
|
|
@@ -143,7 +191,6 @@ export class F1SimulatorApp {
|
|
|
143
191
|
this.replayGhostRenderer = new ReplayGhostRenderer();
|
|
144
192
|
this.pitLaneStatusRenderer = new PitLaneStatusRenderer();
|
|
145
193
|
this.trackSeed = options.trackSeed ?? createMountTrackSeed();
|
|
146
|
-
this.selectedId = this.drivers[0]?.id ?? null;
|
|
147
194
|
this.raceDataBannerConfig = options.ui?.raceDataBanners ?? { initial: 'project', enabled: ['project', 'radio'] };
|
|
148
195
|
this.raceDataBannersMuted = false;
|
|
149
196
|
this.penaltyBannerEnabled = Boolean(options.ui?.penaltyBanners);
|
|
@@ -166,6 +213,7 @@ export class F1SimulatorApp {
|
|
|
166
213
|
canvasHost: this.canvasHost,
|
|
167
214
|
readouts: this.readouts,
|
|
168
215
|
initialMode: options.initialCameraMode,
|
|
216
|
+
driverCamera: options.ui?.driverCamera,
|
|
169
217
|
});
|
|
170
218
|
this.camera = this.cameraController.camera;
|
|
171
219
|
this.overviewMode = 'vehicle';
|
|
@@ -193,7 +241,7 @@ export class F1SimulatorApp {
|
|
|
193
241
|
this.lastLeaderLap = null;
|
|
194
242
|
this.emittedRaceEventKeys = new Set();
|
|
195
243
|
this.raceFinishEmitted = false;
|
|
196
|
-
this.timingGapMode =
|
|
244
|
+
this.timingGapMode = normalizeTimingGapMode(options.ui?.timingGapMode);
|
|
197
245
|
this.telemetryDrawerOpen = Boolean(
|
|
198
246
|
this.readouts.telemetryDrawerWorkbench?.classList?.contains?.('is-telemetry-open'),
|
|
199
247
|
);
|
|
@@ -239,6 +287,7 @@ export class F1SimulatorApp {
|
|
|
239
287
|
);
|
|
240
288
|
this.createCars();
|
|
241
289
|
this.bindControls();
|
|
290
|
+
this.syncTimingPanelDisclosureState();
|
|
242
291
|
this.renderTrack();
|
|
243
292
|
const snapshot = this.sim.snapshot();
|
|
244
293
|
this.updateDom(snapshot);
|
|
@@ -250,7 +299,11 @@ export class F1SimulatorApp {
|
|
|
250
299
|
this.completeComponentLoading();
|
|
251
300
|
this.emitHostCallback('onLoadingChange', { loading: false, phase: 'ready' });
|
|
252
301
|
this.emitHostCallback('onReady', { snapshot });
|
|
253
|
-
this.resizeHandler = () =>
|
|
302
|
+
this.resizeHandler = () => {
|
|
303
|
+
this.syncTimingPanelDisclosureState();
|
|
304
|
+
this.renderSourceSnapshotBuffer ??= {};
|
|
305
|
+
this.applyCamera(this.sim.snapshotRenderInto?.(this.renderSourceSnapshotBuffer) ?? this.sim.snapshotRender?.() ?? this.sim.snapshot());
|
|
306
|
+
};
|
|
254
307
|
window.addEventListener('resize', this.resizeHandler, { signal: this.abortController.signal });
|
|
255
308
|
this.observeLayoutResize();
|
|
256
309
|
if (this.expertMode) {
|
|
@@ -304,7 +357,6 @@ export class F1SimulatorApp {
|
|
|
304
357
|
rules: options.rules,
|
|
305
358
|
participantInteractions: options.participantInteractions,
|
|
306
359
|
replayGhosts: options.replayGhosts,
|
|
307
|
-
trackQueryIndex: options.trackQueryIndex,
|
|
308
360
|
warmup: normalizeWarmupOptions(options.warmup, 'browser'),
|
|
309
361
|
});
|
|
310
362
|
}
|
|
@@ -383,30 +435,42 @@ export class F1SimulatorApp {
|
|
|
383
435
|
|
|
384
436
|
this.timingGapModeButtons.forEach((button) => {
|
|
385
437
|
button.addEventListener('click', () => {
|
|
386
|
-
this.
|
|
387
|
-
this.syncTimingGapModeControls();
|
|
388
|
-
const snapshot = this.sim?.snapshot?.();
|
|
389
|
-
if (snapshot) this.renderTiming(snapshot.cars, snapshot.raceControl.mode, snapshot.penalties ?? []);
|
|
438
|
+
this.toggleTimingGapMode();
|
|
390
439
|
}, eventOptions);
|
|
391
440
|
});
|
|
392
441
|
|
|
393
|
-
this.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
442
|
+
this.timingLists?.forEach?.((timingList) => {
|
|
443
|
+
timingList.addEventListener?.('pointerdown', (event) => {
|
|
444
|
+
const row = event.target instanceof Element ? event.target.closest('[data-driver-id]') : null;
|
|
445
|
+
if (!row) return;
|
|
446
|
+
this.selectCar(row.dataset.driverId, { focus: true });
|
|
447
|
+
}, eventOptions);
|
|
448
|
+
});
|
|
398
449
|
|
|
399
|
-
this.
|
|
450
|
+
this.readouts.timingPanelToggles?.forEach?.((toggle) => {
|
|
451
|
+
toggle.addEventListener?.('click', () => {
|
|
452
|
+
const racePanel = toggle.closest?.('.sim-canvas-panel');
|
|
453
|
+
this.setTimingPanelOpen(!racePanel?.classList?.contains?.('is-timing-panel-open'), racePanel);
|
|
454
|
+
}, eventOptions);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const raceDataOpenButtons = uniqueNodes(this.readouts.raceDataOpens?.length
|
|
458
|
+
? this.readouts.raceDataOpens
|
|
459
|
+
: [this.openButton]);
|
|
460
|
+
raceDataOpenButtons.forEach((button) => button.addEventListener('click', () => {
|
|
400
461
|
const driver = this.driverById.get(this.activeRaceDataId ?? this.selectedId);
|
|
401
462
|
if (!driver) return;
|
|
402
463
|
this.options.onDriverOpen?.(driver);
|
|
403
|
-
}, eventOptions);
|
|
464
|
+
}, eventOptions));
|
|
404
465
|
|
|
405
|
-
this.readouts.
|
|
466
|
+
const raceDataDismissButtons = uniqueNodes(this.readouts.raceDataDismisses?.length
|
|
467
|
+
? this.readouts.raceDataDismisses
|
|
468
|
+
: [this.readouts.raceDataDismiss]);
|
|
469
|
+
raceDataDismissButtons.forEach((button) => button.addEventListener('click', (event) => {
|
|
406
470
|
event.preventDefault();
|
|
407
471
|
event.stopPropagation();
|
|
408
472
|
this.dismissRaceDataPanel(performance.now());
|
|
409
|
-
}, eventOptions);
|
|
473
|
+
}, eventOptions));
|
|
410
474
|
|
|
411
475
|
this.readouts.telemetryDrawerToggle?.addEventListener('click', () => {
|
|
412
476
|
this.setTelemetryDrawerOpen(!this.telemetryDrawerOpen);
|
|
@@ -466,13 +530,51 @@ export class F1SimulatorApp {
|
|
|
466
530
|
}
|
|
467
531
|
this.readouts.telemetryDrawerToggle?.setAttribute?.('aria-expanded', String(this.telemetryDrawerOpen));
|
|
468
532
|
if (this.readouts.telemetryDrawerToggle) {
|
|
469
|
-
this.readouts.telemetryDrawerToggle.textContent = this.telemetryDrawerOpen ? 'Close
|
|
533
|
+
this.readouts.telemetryDrawerToggle.textContent = this.telemetryDrawerOpen ? 'Close' : 'Telemetry';
|
|
534
|
+
this.readouts.telemetryDrawerToggle.setAttribute(
|
|
535
|
+
'aria-label',
|
|
536
|
+
this.telemetryDrawerOpen ? 'Close telemetry' : 'Open telemetry',
|
|
537
|
+
);
|
|
470
538
|
}
|
|
471
539
|
this.invalidateCameraSafeArea();
|
|
472
540
|
this.syncRendererToCurrentLayout({ render: true });
|
|
473
541
|
this.scheduleLayoutResizeSync(380);
|
|
474
542
|
}
|
|
475
543
|
|
|
544
|
+
setTimingPanelOpen(open, racePanel = this.readouts.timingTower?.closest?.('.sim-canvas-panel')) {
|
|
545
|
+
racePanel?.classList?.toggle?.('is-timing-panel-open', Boolean(open));
|
|
546
|
+
this.syncTimingPanelDisclosureState(racePanel);
|
|
547
|
+
this.invalidateCameraSafeArea();
|
|
548
|
+
this.syncRendererToCurrentLayout({ render: true });
|
|
549
|
+
this.scheduleLayoutResizeSync(380);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
isTimingPanelDisclosureMode(toggle = this.readouts.timingPanelToggle) {
|
|
553
|
+
if (!toggle) return false;
|
|
554
|
+
const style = getComputedStyle(toggle);
|
|
555
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
syncTimingPanelDisclosureState(targetPanel = null) {
|
|
559
|
+
const toggles = [...(this.readouts.timingPanelToggles ?? [])];
|
|
560
|
+
for (const toggle of toggles) {
|
|
561
|
+
const racePanel = toggle.closest?.('.sim-canvas-panel');
|
|
562
|
+
if (targetPanel && racePanel !== targetPanel) continue;
|
|
563
|
+
const tower = racePanel?.querySelector?.('[data-timing-tower]');
|
|
564
|
+
const disclosureMode = this.isTimingPanelDisclosureMode(toggle);
|
|
565
|
+
const open = disclosureMode && Boolean(racePanel?.classList?.contains?.('is-timing-panel-open'));
|
|
566
|
+
racePanel?.classList?.toggle?.('is-timing-panel-open', open);
|
|
567
|
+
if (!disclosureMode || open) {
|
|
568
|
+
tower?.removeAttribute?.('inert');
|
|
569
|
+
tower?.setAttribute?.('aria-hidden', 'false');
|
|
570
|
+
} else {
|
|
571
|
+
tower?.setAttribute?.('inert', '');
|
|
572
|
+
tower?.setAttribute?.('aria-hidden', 'true');
|
|
573
|
+
}
|
|
574
|
+
toggle.setAttribute?.('aria-expanded', String(open));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
476
578
|
setRaceDataBannersMuted(muted, now = performance.now()) {
|
|
477
579
|
this.raceDataBannersMuted = Boolean(muted);
|
|
478
580
|
if (this.raceDataBannersMuted) {
|
|
@@ -505,7 +607,8 @@ export class F1SimulatorApp {
|
|
|
505
607
|
|
|
506
608
|
syncRendererToCurrentLayout({ render = false } = {}) {
|
|
507
609
|
this.resizeRendererToCanvasHost();
|
|
508
|
-
|
|
610
|
+
this.renderSourceSnapshotBuffer ??= {};
|
|
611
|
+
const snapshot = this.sim?.snapshotRenderInto?.(this.renderSourceSnapshotBuffer) ?? this.sim?.snapshotRender?.() ?? this.sim?.snapshot?.();
|
|
509
612
|
if (!snapshot) return;
|
|
510
613
|
const renderSnapshot = interpolateRenderSnapshotInto(
|
|
511
614
|
this.renderSnapshotBuffer,
|
|
@@ -537,11 +640,13 @@ export class F1SimulatorApp {
|
|
|
537
640
|
if (typeof ResizeObserver !== 'function') return;
|
|
538
641
|
this.layoutResizeObserver?.disconnect?.();
|
|
539
642
|
this.layoutResizeObserver = new ResizeObserver(() => {
|
|
643
|
+
this.syncTimingPanelDisclosureState();
|
|
540
644
|
this.invalidateCameraSafeArea();
|
|
541
645
|
this.syncRendererToCurrentLayout({ render: true });
|
|
542
646
|
});
|
|
543
647
|
if (this.canvasHost) this.layoutResizeObserver.observe(this.canvasHost);
|
|
544
|
-
|
|
648
|
+
resolveNodes(this.readouts.timingTowers, this.readouts.timingTower)
|
|
649
|
+
.forEach((timingTower) => this.layoutResizeObserver.observe(timingTower));
|
|
545
650
|
}
|
|
546
651
|
|
|
547
652
|
invalidateCameraSafeArea() {
|
|
@@ -572,6 +677,7 @@ export class F1SimulatorApp {
|
|
|
572
677
|
renderExpertFrame(snapshot = this.sim?.snapshot(), { forceDomUpdate = false, observation } = {}) {
|
|
573
678
|
if (!snapshot) return;
|
|
574
679
|
const now = performance.now();
|
|
680
|
+
this.sampleFps(now);
|
|
575
681
|
const renderSnapshot = interpolateRenderSnapshotInto(this.renderSnapshotBuffer, snapshot, 0);
|
|
576
682
|
this.applyCamera(renderSnapshot);
|
|
577
683
|
this.renderDrsTrails(renderSnapshot);
|
|
@@ -670,6 +776,7 @@ export class F1SimulatorApp {
|
|
|
670
776
|
}
|
|
671
777
|
|
|
672
778
|
updateDom(snapshot, { emitLifecycle = true } = {}) {
|
|
779
|
+
snapshot = normalizeRuntimeSnapshot(snapshot, this.totalLaps);
|
|
673
780
|
const leader = snapshot.cars[0];
|
|
674
781
|
const selected = snapshot.cars.find((car) => car.id === this.selectedId) ?? leader;
|
|
675
782
|
const activeDrs = snapshot.cars.filter((car) => car.drsActive).length;
|
|
@@ -677,6 +784,7 @@ export class F1SimulatorApp {
|
|
|
677
784
|
const now = performance.now();
|
|
678
785
|
|
|
679
786
|
if (emitLifecycle) this.emitSnapshotLifecycle(snapshot);
|
|
787
|
+
this.syncThemeContext(snapshot);
|
|
680
788
|
|
|
681
789
|
if (
|
|
682
790
|
this.shouldAutoHideActiveRaceData() &&
|
|
@@ -717,7 +825,7 @@ export class F1SimulatorApp {
|
|
|
717
825
|
this.lastTimingPenaltyKey = timingPenaltyKey;
|
|
718
826
|
this.lastTimingOrderKey = timingOrderKey;
|
|
719
827
|
}
|
|
720
|
-
this.renderTelemetry(selected);
|
|
828
|
+
if (selected) this.renderTelemetry(selected);
|
|
721
829
|
const activeRaceDataCar = this.activeRaceDataId
|
|
722
830
|
? snapshot.cars.find((car) => car.id === this.activeRaceDataId)
|
|
723
831
|
: null;
|
|
@@ -804,7 +912,8 @@ export class F1SimulatorApp {
|
|
|
804
912
|
|
|
805
913
|
renderTiming(cars, raceMode, penalties = []) {
|
|
806
914
|
this.syncTimingGapModeControls();
|
|
807
|
-
this.lastTimingMarkup =
|
|
915
|
+
this.lastTimingMarkup = renderTimingTowers({
|
|
916
|
+
timingLists: this.timingLists,
|
|
808
917
|
timingList: this.timingList,
|
|
809
918
|
cars,
|
|
810
919
|
raceMode,
|
|
@@ -817,6 +926,35 @@ export class F1SimulatorApp {
|
|
|
817
926
|
});
|
|
818
927
|
}
|
|
819
928
|
|
|
929
|
+
setTimingGapMode(mode) {
|
|
930
|
+
const nextMode = normalizeTimingGapMode(mode);
|
|
931
|
+
if (this.timingGapMode === nextMode) {
|
|
932
|
+
this.syncTimingGapModeControls();
|
|
933
|
+
return this.timingGapMode;
|
|
934
|
+
}
|
|
935
|
+
this.timingGapMode = nextMode;
|
|
936
|
+
this.options = {
|
|
937
|
+
...this.options,
|
|
938
|
+
ui: {
|
|
939
|
+
...this.options.ui,
|
|
940
|
+
timingGapMode: nextMode,
|
|
941
|
+
},
|
|
942
|
+
};
|
|
943
|
+
this.lastTimingRenderTime = 0;
|
|
944
|
+
this.syncTimingGapModeControls();
|
|
945
|
+
const snapshot = this.sim?.snapshot?.();
|
|
946
|
+
if (snapshot) this.renderTiming(snapshot.cars, snapshot.raceControl.mode, snapshot.penalties ?? []);
|
|
947
|
+
return this.timingGapMode;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
getTimingGapMode() {
|
|
951
|
+
return this.timingGapMode;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
toggleTimingGapMode() {
|
|
955
|
+
return this.setTimingGapMode(getNextTimingGapMode(this.timingGapMode));
|
|
956
|
+
}
|
|
957
|
+
|
|
820
958
|
getTimingOrderKey(cars = []) {
|
|
821
959
|
return getTimingOrderKey(cars, { timingGapMode: this.timingGapMode });
|
|
822
960
|
}
|
|
@@ -1050,6 +1188,39 @@ export class F1SimulatorApp {
|
|
|
1050
1188
|
updateToggleButtons(this.safetyButtons, active);
|
|
1051
1189
|
}
|
|
1052
1190
|
|
|
1191
|
+
syncThemeContext(snapshot = null) {
|
|
1192
|
+
const selectedTeamId = this.getSelectedTeamId(snapshot);
|
|
1193
|
+
const contextKey = selectedTeamId ?? '';
|
|
1194
|
+
if (this.lastThemeContextKey === contextKey) return;
|
|
1195
|
+
this.lastThemeContextKey = contextKey;
|
|
1196
|
+
const context = { selectedTeamId };
|
|
1197
|
+
if (typeof this.root?.applyCssVariables === 'function') {
|
|
1198
|
+
this.root.applyCssVariables(context);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
applyPaddockThemeCssVariables(this.root, this.options.theme, context);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
setTheme(theme) {
|
|
1205
|
+
this.options = {
|
|
1206
|
+
...this.options,
|
|
1207
|
+
theme,
|
|
1208
|
+
};
|
|
1209
|
+
this.lastThemeContextKey = null;
|
|
1210
|
+
this.syncThemeContext(this.getSnapshot());
|
|
1211
|
+
return this.options.theme;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
getTheme() {
|
|
1215
|
+
return this.options.theme;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
getSelectedTeamId(snapshot = null) {
|
|
1219
|
+
const selectedCar = snapshot?.cars?.find?.((car) => car.id === this.selectedId);
|
|
1220
|
+
const selectedDriver = this.driverById.get(this.selectedId);
|
|
1221
|
+
return selectedCar?.team?.id ?? selectedDriver?.team?.id ?? null;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1053
1224
|
applyExpertOptions(nextOptions = {}) {
|
|
1054
1225
|
this.options = {
|
|
1055
1226
|
...this.options,
|
|
@@ -1071,6 +1242,7 @@ export class F1SimulatorApp {
|
|
|
1071
1242
|
},
|
|
1072
1243
|
},
|
|
1073
1244
|
};
|
|
1245
|
+
this.totalLaps = this.options.totalLaps;
|
|
1074
1246
|
this.assets = this.options.assets;
|
|
1075
1247
|
if (hasOwnOption(nextOptions, 'trackSeed')) {
|
|
1076
1248
|
this.trackSeed = this.options.trackSeed ?? createMountTrackSeed();
|
|
@@ -1079,6 +1251,7 @@ export class F1SimulatorApp {
|
|
|
1079
1251
|
this.drivers = nextOptions.drivers;
|
|
1080
1252
|
this.driverById = new Map(this.drivers.map((driver) => [driver.id, driver]));
|
|
1081
1253
|
this.root.style.setProperty('--paddock-entry-count', String(this.drivers.length));
|
|
1254
|
+
this.lastThemeContextKey = null;
|
|
1082
1255
|
this.createCars();
|
|
1083
1256
|
}
|
|
1084
1257
|
}
|
|
@@ -1096,13 +1269,16 @@ export class F1SimulatorApp {
|
|
|
1096
1269
|
this.raceDataBannerConfig = this.options.ui?.raceDataBanners ?? this.raceDataBannerConfig;
|
|
1097
1270
|
this.penaltyBannerEnabled = Boolean(this.options.ui?.penaltyBanners);
|
|
1098
1271
|
this.timingPenaltyBadgesEnabled = Boolean(this.options.ui?.timingPenaltyBadges);
|
|
1099
|
-
this.
|
|
1100
|
-
|
|
1272
|
+
this.timingGapMode = normalizeTimingGapMode(this.options.ui?.timingGapMode);
|
|
1273
|
+
this.cameraController.driverCamera = Boolean(this.options.ui?.driverCamera);
|
|
1274
|
+
this.root.style.setProperty('--broadcast-panel-surface', formatCssUrl(this.assets.broadcastPanel));
|
|
1275
|
+
this.selectedId = this.drivers[0]?.id ?? null;
|
|
1276
|
+
this.lastThemeContextKey = null;
|
|
1277
|
+
this.syncThemeContext();
|
|
1101
1278
|
this.sim = this.createRaceSimulation();
|
|
1102
1279
|
if (this.expertMode) {
|
|
1103
1280
|
this.expert = createBrowserExpertAdapter(this, this.options.expert);
|
|
1104
1281
|
}
|
|
1105
|
-
this.selectedId = this.drivers[0]?.id ?? null;
|
|
1106
1282
|
this.lastTimingMarkup = null;
|
|
1107
1283
|
this.lastOverviewRenderKey = null;
|
|
1108
1284
|
this.lastFinishClassificationMarkup = null;
|
|
@@ -1177,8 +1353,16 @@ export class F1SimulatorApp {
|
|
|
1177
1353
|
cancelAnimationFrame(this.layoutResizeFrame);
|
|
1178
1354
|
}
|
|
1179
1355
|
this.layoutResizeFrame = null;
|
|
1356
|
+
if (this.runtimeVisibilitySyncFrame && typeof cancelAnimationFrame === 'function') {
|
|
1357
|
+
cancelAnimationFrame(this.runtimeVisibilitySyncFrame);
|
|
1358
|
+
}
|
|
1359
|
+
this.runtimeVisibilitySyncFrame = null;
|
|
1180
1360
|
this.layoutResizeObserver?.disconnect?.();
|
|
1181
1361
|
this.layoutResizeObserver = null;
|
|
1362
|
+
this.layoutSupportCleanup?.();
|
|
1363
|
+
this.layoutSupportCleanup = null;
|
|
1364
|
+
this.raceOverlayClearanceCleanup?.();
|
|
1365
|
+
this.raceOverlayClearanceCleanup = null;
|
|
1182
1366
|
this.visibilityObserver?.disconnect?.();
|
|
1183
1367
|
this.visibilityObserver = null;
|
|
1184
1368
|
if (this.app && this.tickerCallback) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatDriverNumber } from '../../data/championship.js';
|
|
2
|
-
import {
|
|
2
|
+
import { formatCssColor } from '../../config/cssValues.js';
|
|
3
|
+
import { setStyleProperty, setText } from '../domBindings.js';
|
|
3
4
|
|
|
4
5
|
export const RACE_DATA_SELECTED_VISIBLE_MS = 5200;
|
|
5
6
|
export const RADIO_BREAK_MIN_MS = 4800;
|
|
@@ -8,47 +9,80 @@ export const RADIO_VISIBLE_MIN_MS = 6200;
|
|
|
8
9
|
export const RADIO_VISIBLE_MAX_MS = 9200;
|
|
9
10
|
export const RADIO_SCHEDULE_CATCHUP_LIMIT_MS = 30000;
|
|
10
11
|
|
|
12
|
+
function getRaceDataPanelBindings(readouts) {
|
|
13
|
+
if (readouts.raceDataPanelBindings?.length) return readouts.raceDataPanelBindings;
|
|
14
|
+
if (!readouts.raceDataPanel) return [];
|
|
15
|
+
return [{
|
|
16
|
+
panel: readouts.raceDataPanel,
|
|
17
|
+
kicker: readouts.raceDataKicker,
|
|
18
|
+
title: readouts.raceDataTitle,
|
|
19
|
+
number: readouts.raceDataNumber,
|
|
20
|
+
subtitle: readouts.raceDataSubtitle,
|
|
21
|
+
open: readouts.raceDataOpen,
|
|
22
|
+
dismiss: readouts.raceDataDismiss,
|
|
23
|
+
}];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasTelemetryDetailPanel(readouts) {
|
|
27
|
+
return getRaceDataPanelBindings(readouts).some((binding) => (
|
|
28
|
+
binding.panel?.classList?.contains?.('race-data-panel--with-telemetry')
|
|
29
|
+
));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clearRaceDataIdleMode(panel) {
|
|
33
|
+
panel?.removeAttribute?.('data-idle-mode');
|
|
34
|
+
if (panel?.dataset) delete panel.dataset.idleMode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setRaceDataIdleMode(panel, mode) {
|
|
38
|
+
if (panel?.dataset) {
|
|
39
|
+
panel.dataset.idleMode = mode;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
panel?.setAttribute?.('data-idle-mode', mode);
|
|
43
|
+
}
|
|
44
|
+
|
|
11
45
|
export function shouldAutoHideActiveRaceData({ activeRaceDataId, options, readouts }) {
|
|
12
46
|
return Boolean(
|
|
13
47
|
activeRaceDataId &&
|
|
14
48
|
!(
|
|
15
49
|
options.ui?.raceDataTelemetryDetail ||
|
|
16
|
-
readouts
|
|
50
|
+
hasTelemetryDetailPanel(readouts)
|
|
17
51
|
),
|
|
18
52
|
);
|
|
19
53
|
}
|
|
20
54
|
|
|
21
55
|
export function renderRaceData({ car, drivers, readouts, options }) {
|
|
22
|
-
|
|
56
|
+
const panels = getRaceDataPanelBindings(readouts);
|
|
57
|
+
if (!car || !panels.length) return false;
|
|
23
58
|
if (typeof options.isRaceDataBannerEnabled === 'function' && !options.isRaceDataBannerEnabled('project')) return false;
|
|
24
59
|
const driver = drivers.find((item) => item.id === car.id);
|
|
25
60
|
if (!driver) return false;
|
|
26
61
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
readouts.raceDataOpen.hidden = typeof options.onDriverOpen !== 'function';
|
|
42
|
-
}
|
|
62
|
+
panels.forEach((binding) => {
|
|
63
|
+
setStyleProperty(binding.panel, '--driver-color', formatCssColor(driver.color));
|
|
64
|
+
binding.panel.classList.remove('is-hidden');
|
|
65
|
+
binding.panel.classList.add('is-project-mode');
|
|
66
|
+
binding.panel.classList.remove('is-radio-mode');
|
|
67
|
+
clearRaceDataIdleMode(binding.panel);
|
|
68
|
+
setText(binding.kicker, 'Project');
|
|
69
|
+
setText(binding.title, driver.name);
|
|
70
|
+
setText(binding.number, formatDriverNumber(car.driverNumber ?? driver.driverNumber));
|
|
71
|
+
setText(binding.subtitle, `${car.code} - P${car.rank} - ${driver.raceData?.[0] ?? 'Project entry'}`);
|
|
72
|
+
if (binding.open) {
|
|
73
|
+
binding.open.hidden = typeof options.onDriverOpen !== 'function';
|
|
74
|
+
}
|
|
75
|
+
});
|
|
43
76
|
return true;
|
|
44
77
|
}
|
|
45
78
|
|
|
46
79
|
export function hideRaceDataPanel(readouts) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
80
|
+
getRaceDataPanelBindings(readouts).forEach((binding) => {
|
|
81
|
+
binding.panel.classList.add('is-hidden');
|
|
82
|
+
binding.panel.classList.remove('is-project-mode', 'is-radio-mode');
|
|
83
|
+
clearRaceDataIdleMode(binding.panel);
|
|
84
|
+
if (binding.open) binding.open.hidden = true;
|
|
85
|
+
});
|
|
52
86
|
}
|
|
53
87
|
|
|
54
88
|
export function getProjectRadioQuote({ drivers, radioState }) {
|
|
@@ -56,24 +90,25 @@ export function getProjectRadioQuote({ drivers, radioState }) {
|
|
|
56
90
|
const quote = driver.raceData?.[radioState.quoteIndex] ?? 'Project entry';
|
|
57
91
|
|
|
58
92
|
return {
|
|
59
|
-
color: driver.color,
|
|
93
|
+
color: formatCssColor(driver.color),
|
|
60
94
|
title: driver.name,
|
|
61
95
|
subtitle: `${driver.code} - "${quote}"`,
|
|
62
96
|
};
|
|
63
97
|
}
|
|
64
98
|
|
|
65
99
|
export function renderProjectRadio({ readouts, radio }) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
getRaceDataPanelBindings(readouts).forEach((binding) => {
|
|
101
|
+
setStyleProperty(binding.panel, '--driver-color', formatCssColor(radio.color));
|
|
102
|
+
binding.panel.classList.remove('is-hidden');
|
|
103
|
+
binding.panel.classList.add('is-radio-mode');
|
|
104
|
+
binding.panel.classList.remove('is-project-mode');
|
|
105
|
+
setRaceDataIdleMode(binding.panel, 'quote');
|
|
106
|
+
setText(binding.kicker, 'Project radio');
|
|
107
|
+
setText(binding.title, radio.title);
|
|
108
|
+
setText(binding.number, '');
|
|
109
|
+
setText(binding.subtitle, radio.subtitle);
|
|
110
|
+
if (binding.open) binding.open.hidden = true;
|
|
111
|
+
});
|
|
77
112
|
}
|
|
78
113
|
|
|
79
114
|
export function randomRadioRange(nextRandom, min, max) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatCssColor } from '../../config/cssValues.js';
|
|
2
|
+
import { setStyleProperty, setText } from '../domBindings.js';
|
|
2
3
|
import {
|
|
3
4
|
formatPenaltyChip,
|
|
4
5
|
formatPenaltyHeadline,
|
|
@@ -18,7 +19,7 @@ export function createPenaltyStewardMessage(penalty, driverById) {
|
|
|
18
19
|
driverId: penalty.driverId,
|
|
19
20
|
penaltyId: penalty.id,
|
|
20
21
|
time: penalty.time,
|
|
21
|
-
color: driver?.color
|
|
22
|
+
color: formatCssColor(driver?.color),
|
|
22
23
|
kicker: formatPenaltyChip(penalty),
|
|
23
24
|
title: `${code} ${formatPenaltyHeadline(penalty).replace(/^\+\d+ seconds? /u, '')}`,
|
|
24
25
|
detail: `${formatPenaltyType(penalty.type)} - ${penalty.reason ?? 'Steward decision'}`,
|
|
@@ -39,7 +40,7 @@ export function createWarningStewardMessage(event, driverById) {
|
|
|
39
40
|
driverId: event.carId,
|
|
40
41
|
violationCount: event.violationCount,
|
|
41
42
|
time: event.at,
|
|
42
|
-
color: driver?.color
|
|
43
|
+
color: formatCssColor(driver?.color, '#ffd166'),
|
|
43
44
|
kicker: 'Warning',
|
|
44
45
|
title: `${code} ${formatPenaltyType(event.type).toLowerCase()}`,
|
|
45
46
|
detail: `${formatPenaltyType(event.type)}${warningProgress}`,
|
|
@@ -90,7 +91,7 @@ export function renderActiveStewardMessage(readouts, activeStewardMessage) {
|
|
|
90
91
|
panel.classList.remove('is-penalty', 'is-warning');
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
|
-
panel
|
|
94
|
+
setStyleProperty(panel, '--steward-color', formatCssColor(message.color));
|
|
94
95
|
panel.classList.remove('is-hidden', 'is-penalty', 'is-warning');
|
|
95
96
|
panel.classList.add(message.kind === 'penalty' ? 'is-penalty' : 'is-warning');
|
|
96
97
|
setText(readouts.stewardMessageKicker, message.kicker);
|
|
@@ -3,10 +3,12 @@ import { VEHICLE_GEOMETRY } from '../../simulation/vehicleGeometry.js';
|
|
|
3
3
|
|
|
4
4
|
export const CAMERA_TARGET_LERP = 0.12;
|
|
5
5
|
export const CAMERA_SCALE_LERP = 0.12;
|
|
6
|
+
export const CAMERA_ROTATION_LERP = 0.16;
|
|
6
7
|
export const CAMERA_PRESETS = {
|
|
7
8
|
overview: 1,
|
|
8
9
|
leader: 18,
|
|
9
10
|
selected: 24,
|
|
11
|
+
driver: 34,
|
|
10
12
|
'show-all': 1,
|
|
11
13
|
pit: 2.35,
|
|
12
14
|
};
|