@inventure71/paddockjs 2.0.0 → 3.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 +37 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +92 -17
- package/docs/architecture.md +26 -16
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +7 -5
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +121 -33
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +6 -6
- package/docs/rules.md +39 -9
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +76 -20
- package/docs/training.md +27 -20
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +75 -36
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +190 -68
- 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 +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/restartOptions.js +34 -0
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +34 -5
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +17 -4
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/options.js +1 -1
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +65 -25
- package/src/environment/scenarios.js +22 -3
- package/src/environment/sensors/index.js +3 -3
- package/src/environment/sensors/opponentRadar.js +13 -3
- package/src/environment/sensors/sensorTargets.js +8 -2
- package/src/environment/sensors/surfaceRays.js +84 -12
- package/src/environment/sensors/trackRays.js +173 -15
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +112 -9
- package/src/index.js +26 -33
- 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 +5 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitQueue.js +5 -0
- package/src/simulation/pit/pitRouteMovement.js +7 -0
- package/src/simulation/pit/pitRouting.js +2 -4
- package/src/simulation/pit/pitService.js +2 -0
- package/src/simulation/pit/pitTireService.js +2 -0
- package/src/simulation/race/raceLifecycle.js +2 -6
- package/src/simulation/race/raceProgress.js +6 -1
- package/src/simulation/race/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +2 -2
- package/src/simulation/race/redFlag.js +3 -3
- package/src/simulation/race/safetyCar.js +10 -4
- package/src/simulation/race/stalledDnf.js +8 -17
- package/src/simulation/raceSimulation.js +1 -0
- package/src/simulation/rules/rulesReview.js +3 -0
- package/src/simulation/snapshots/raceSnapshots.js +1 -0
- package/src/simulation/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +193 -434
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -0
- package/src/simulation/track/trackStatePolicy.js +0 -1
- package/src/simulation/vehicle/contactResolution.js +67 -2
- package/src/simulation/vehicle/runoffResponse.js +11 -12
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +5 -4
- package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- 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';
|
|
@@ -16,17 +20,12 @@ import { PitLaneStatusRenderer } from './rendering/pitLaneStatusRenderer.js';
|
|
|
16
20
|
import { ReplayGhostRenderer } from './rendering/replayGhostRenderer.js';
|
|
17
21
|
import { renderTrackSurface } from './rendering/trackRenderer.js';
|
|
18
22
|
import {
|
|
19
|
-
formatLapGap,
|
|
20
|
-
formatRaceGap,
|
|
21
|
-
getPenaltyByDriver,
|
|
22
23
|
getTimingOrderKey,
|
|
23
24
|
getTimingPenaltyKey,
|
|
24
|
-
|
|
25
|
-
renderTimingTower,
|
|
25
|
+
renderTimingTowers,
|
|
26
26
|
syncTimingGapModeControls,
|
|
27
27
|
} from './readouts/timingTowerRenderer.js';
|
|
28
|
-
import {
|
|
29
|
-
import { getOverviewFields, renderCarDriverOverview } from './readouts/carOverviewRenderer.js';
|
|
28
|
+
import { renderCarDriverOverview } from './readouts/carOverviewRenderer.js';
|
|
30
29
|
import { renderLapTelemetry, renderTelemetryReadouts } from './readouts/telemetryRenderer.js';
|
|
31
30
|
import {
|
|
32
31
|
renderRaceFinish,
|
|
@@ -65,6 +64,44 @@ import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
|
|
|
65
64
|
const SIMULATION_SPEED_STEPS = [1, 2, 3, 4, 5, 10];
|
|
66
65
|
const DRS_DRAG_REDUCTION_PERCENT = 58;
|
|
67
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
|
+
|
|
68
105
|
function createMountTrackSeed() {
|
|
69
106
|
const values = new Uint32Array(1);
|
|
70
107
|
try {
|
|
@@ -110,16 +147,22 @@ export class F1SimulatorApp {
|
|
|
110
147
|
this.drivers = options.drivers;
|
|
111
148
|
this.driverById = new Map(this.drivers.map((driver) => [driver.id, driver]));
|
|
112
149
|
this.assets = options.assets;
|
|
113
|
-
this.root.style.setProperty('--broadcast-panel-surface',
|
|
150
|
+
this.root.style.setProperty('--broadcast-panel-surface', formatCssUrl(this.assets.broadcastPanel));
|
|
114
151
|
this.root.style.setProperty('--paddock-entry-count', String(this.drivers.length));
|
|
115
|
-
|
|
152
|
+
this.totalLaps = options.totalLaps;
|
|
153
|
+
this.selectedId = this.drivers[0]?.id ?? null;
|
|
154
|
+
this.lastThemeContextKey = null;
|
|
155
|
+
this.syncThemeContext();
|
|
116
156
|
Object.assign(this, querySimulatorDom(root));
|
|
117
157
|
this.abortController = new AbortController();
|
|
118
158
|
this.resizeHandler = null;
|
|
119
159
|
this.layoutResizeObserver = null;
|
|
160
|
+
this.layoutSupportCleanup = installLayoutSupport(root);
|
|
161
|
+
this.raceOverlayClearanceCleanup = installRaceOverlayClearanceSupport(root);
|
|
120
162
|
this.cameraController = null;
|
|
121
163
|
this.visibilityChangeHandler = null;
|
|
122
164
|
this.visibilityObserver = null;
|
|
165
|
+
this.runtimeVisibilitySyncFrame = null;
|
|
123
166
|
this.tickerCallback = null;
|
|
124
167
|
this.expert = null;
|
|
125
168
|
this.expertMode = Boolean(options.expert?.enabled);
|
|
@@ -148,7 +191,6 @@ export class F1SimulatorApp {
|
|
|
148
191
|
this.replayGhostRenderer = new ReplayGhostRenderer();
|
|
149
192
|
this.pitLaneStatusRenderer = new PitLaneStatusRenderer();
|
|
150
193
|
this.trackSeed = options.trackSeed ?? createMountTrackSeed();
|
|
151
|
-
this.selectedId = this.drivers[0]?.id ?? null;
|
|
152
194
|
this.raceDataBannerConfig = options.ui?.raceDataBanners ?? { initial: 'project', enabled: ['project', 'radio'] };
|
|
153
195
|
this.raceDataBannersMuted = false;
|
|
154
196
|
this.penaltyBannerEnabled = Boolean(options.ui?.penaltyBanners);
|
|
@@ -171,6 +213,7 @@ export class F1SimulatorApp {
|
|
|
171
213
|
canvasHost: this.canvasHost,
|
|
172
214
|
readouts: this.readouts,
|
|
173
215
|
initialMode: options.initialCameraMode,
|
|
216
|
+
driverCamera: options.ui?.driverCamera,
|
|
174
217
|
});
|
|
175
218
|
this.camera = this.cameraController.camera;
|
|
176
219
|
this.overviewMode = 'vehicle';
|
|
@@ -198,7 +241,7 @@ export class F1SimulatorApp {
|
|
|
198
241
|
this.lastLeaderLap = null;
|
|
199
242
|
this.emittedRaceEventKeys = new Set();
|
|
200
243
|
this.raceFinishEmitted = false;
|
|
201
|
-
this.timingGapMode =
|
|
244
|
+
this.timingGapMode = normalizeTimingGapMode(options.ui?.timingGapMode);
|
|
202
245
|
this.telemetryDrawerOpen = Boolean(
|
|
203
246
|
this.readouts.telemetryDrawerWorkbench?.classList?.contains?.('is-telemetry-open'),
|
|
204
247
|
);
|
|
@@ -244,6 +287,7 @@ export class F1SimulatorApp {
|
|
|
244
287
|
);
|
|
245
288
|
this.createCars();
|
|
246
289
|
this.bindControls();
|
|
290
|
+
this.syncTimingPanelDisclosureState();
|
|
247
291
|
this.renderTrack();
|
|
248
292
|
const snapshot = this.sim.snapshot();
|
|
249
293
|
this.updateDom(snapshot);
|
|
@@ -255,7 +299,10 @@ export class F1SimulatorApp {
|
|
|
255
299
|
this.completeComponentLoading();
|
|
256
300
|
this.emitHostCallback('onLoadingChange', { loading: false, phase: 'ready' });
|
|
257
301
|
this.emitHostCallback('onReady', { snapshot });
|
|
258
|
-
this.resizeHandler = () =>
|
|
302
|
+
this.resizeHandler = () => {
|
|
303
|
+
this.syncTimingPanelDisclosureState();
|
|
304
|
+
this.applyCamera(this.sim.snapshotRender?.() ?? this.sim.snapshot());
|
|
305
|
+
};
|
|
259
306
|
window.addEventListener('resize', this.resizeHandler, { signal: this.abortController.signal });
|
|
260
307
|
this.observeLayoutResize();
|
|
261
308
|
if (this.expertMode) {
|
|
@@ -309,7 +356,6 @@ export class F1SimulatorApp {
|
|
|
309
356
|
rules: options.rules,
|
|
310
357
|
participantInteractions: options.participantInteractions,
|
|
311
358
|
replayGhosts: options.replayGhosts,
|
|
312
|
-
trackQueryIndex: options.trackQueryIndex,
|
|
313
359
|
warmup: normalizeWarmupOptions(options.warmup, 'browser'),
|
|
314
360
|
});
|
|
315
361
|
}
|
|
@@ -327,18 +373,6 @@ export class F1SimulatorApp {
|
|
|
327
373
|
});
|
|
328
374
|
}
|
|
329
375
|
|
|
330
|
-
createRawCarGeometryGraphic(driver) {
|
|
331
|
-
return this.carRenderer.createRawCarGeometryGraphic(driver);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
createServiceCountdownLabel() {
|
|
335
|
-
return this.carRenderer.createServiceCountdownLabel();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
applyServiceCountdownTone(label, tone) {
|
|
339
|
-
this.carRenderer.applyServiceCountdownTone(label, tone);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
376
|
bindControls() {
|
|
343
377
|
const eventOptions = { signal: this.abortController.signal };
|
|
344
378
|
|
|
@@ -400,30 +434,42 @@ export class F1SimulatorApp {
|
|
|
400
434
|
|
|
401
435
|
this.timingGapModeButtons.forEach((button) => {
|
|
402
436
|
button.addEventListener('click', () => {
|
|
403
|
-
this.
|
|
404
|
-
this.syncTimingGapModeControls();
|
|
405
|
-
const snapshot = this.sim?.snapshot?.();
|
|
406
|
-
if (snapshot) this.renderTiming(snapshot.cars, snapshot.raceControl.mode, snapshot.penalties ?? []);
|
|
437
|
+
this.toggleTimingGapMode();
|
|
407
438
|
}, eventOptions);
|
|
408
439
|
});
|
|
409
440
|
|
|
410
|
-
this.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
441
|
+
this.timingLists?.forEach?.((timingList) => {
|
|
442
|
+
timingList.addEventListener?.('pointerdown', (event) => {
|
|
443
|
+
const row = event.target instanceof Element ? event.target.closest('[data-driver-id]') : null;
|
|
444
|
+
if (!row) return;
|
|
445
|
+
this.selectCar(row.dataset.driverId, { focus: true });
|
|
446
|
+
}, eventOptions);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
this.readouts.timingPanelToggles?.forEach?.((toggle) => {
|
|
450
|
+
toggle.addEventListener?.('click', () => {
|
|
451
|
+
const racePanel = toggle.closest?.('.sim-canvas-panel');
|
|
452
|
+
this.setTimingPanelOpen(!racePanel?.classList?.contains?.('is-timing-panel-open'), racePanel);
|
|
453
|
+
}, eventOptions);
|
|
454
|
+
});
|
|
415
455
|
|
|
416
|
-
this.
|
|
456
|
+
const raceDataOpenButtons = uniqueNodes(this.readouts.raceDataOpens?.length
|
|
457
|
+
? this.readouts.raceDataOpens
|
|
458
|
+
: [this.openButton]);
|
|
459
|
+
raceDataOpenButtons.forEach((button) => button.addEventListener('click', () => {
|
|
417
460
|
const driver = this.driverById.get(this.activeRaceDataId ?? this.selectedId);
|
|
418
461
|
if (!driver) return;
|
|
419
462
|
this.options.onDriverOpen?.(driver);
|
|
420
|
-
}, eventOptions);
|
|
463
|
+
}, eventOptions));
|
|
421
464
|
|
|
422
|
-
this.readouts.
|
|
465
|
+
const raceDataDismissButtons = uniqueNodes(this.readouts.raceDataDismisses?.length
|
|
466
|
+
? this.readouts.raceDataDismisses
|
|
467
|
+
: [this.readouts.raceDataDismiss]);
|
|
468
|
+
raceDataDismissButtons.forEach((button) => button.addEventListener('click', (event) => {
|
|
423
469
|
event.preventDefault();
|
|
424
470
|
event.stopPropagation();
|
|
425
471
|
this.dismissRaceDataPanel(performance.now());
|
|
426
|
-
}, eventOptions);
|
|
472
|
+
}, eventOptions));
|
|
427
473
|
|
|
428
474
|
this.readouts.telemetryDrawerToggle?.addEventListener('click', () => {
|
|
429
475
|
this.setTelemetryDrawerOpen(!this.telemetryDrawerOpen);
|
|
@@ -483,13 +529,51 @@ export class F1SimulatorApp {
|
|
|
483
529
|
}
|
|
484
530
|
this.readouts.telemetryDrawerToggle?.setAttribute?.('aria-expanded', String(this.telemetryDrawerOpen));
|
|
485
531
|
if (this.readouts.telemetryDrawerToggle) {
|
|
486
|
-
this.readouts.telemetryDrawerToggle.textContent = this.telemetryDrawerOpen ? 'Close
|
|
532
|
+
this.readouts.telemetryDrawerToggle.textContent = this.telemetryDrawerOpen ? 'Close' : 'Telemetry';
|
|
533
|
+
this.readouts.telemetryDrawerToggle.setAttribute(
|
|
534
|
+
'aria-label',
|
|
535
|
+
this.telemetryDrawerOpen ? 'Close telemetry' : 'Open telemetry',
|
|
536
|
+
);
|
|
487
537
|
}
|
|
488
538
|
this.invalidateCameraSafeArea();
|
|
489
539
|
this.syncRendererToCurrentLayout({ render: true });
|
|
490
540
|
this.scheduleLayoutResizeSync(380);
|
|
491
541
|
}
|
|
492
542
|
|
|
543
|
+
setTimingPanelOpen(open, racePanel = this.readouts.timingTower?.closest?.('.sim-canvas-panel')) {
|
|
544
|
+
racePanel?.classList?.toggle?.('is-timing-panel-open', Boolean(open));
|
|
545
|
+
this.syncTimingPanelDisclosureState(racePanel);
|
|
546
|
+
this.invalidateCameraSafeArea();
|
|
547
|
+
this.syncRendererToCurrentLayout({ render: true });
|
|
548
|
+
this.scheduleLayoutResizeSync(380);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
isTimingPanelDisclosureMode(toggle = this.readouts.timingPanelToggle) {
|
|
552
|
+
if (!toggle) return false;
|
|
553
|
+
const style = getComputedStyle(toggle);
|
|
554
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
syncTimingPanelDisclosureState(targetPanel = null) {
|
|
558
|
+
const toggles = [...(this.readouts.timingPanelToggles ?? [])];
|
|
559
|
+
for (const toggle of toggles) {
|
|
560
|
+
const racePanel = toggle.closest?.('.sim-canvas-panel');
|
|
561
|
+
if (targetPanel && racePanel !== targetPanel) continue;
|
|
562
|
+
const tower = racePanel?.querySelector?.('[data-timing-tower]');
|
|
563
|
+
const disclosureMode = this.isTimingPanelDisclosureMode(toggle);
|
|
564
|
+
const open = disclosureMode && Boolean(racePanel?.classList?.contains?.('is-timing-panel-open'));
|
|
565
|
+
racePanel?.classList?.toggle?.('is-timing-panel-open', open);
|
|
566
|
+
if (!disclosureMode || open) {
|
|
567
|
+
tower?.removeAttribute?.('inert');
|
|
568
|
+
tower?.setAttribute?.('aria-hidden', 'false');
|
|
569
|
+
} else {
|
|
570
|
+
tower?.setAttribute?.('inert', '');
|
|
571
|
+
tower?.setAttribute?.('aria-hidden', 'true');
|
|
572
|
+
}
|
|
573
|
+
toggle.setAttribute?.('aria-expanded', String(open));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
493
577
|
setRaceDataBannersMuted(muted, now = performance.now()) {
|
|
494
578
|
this.raceDataBannersMuted = Boolean(muted);
|
|
495
579
|
if (this.raceDataBannersMuted) {
|
|
@@ -554,11 +638,13 @@ export class F1SimulatorApp {
|
|
|
554
638
|
if (typeof ResizeObserver !== 'function') return;
|
|
555
639
|
this.layoutResizeObserver?.disconnect?.();
|
|
556
640
|
this.layoutResizeObserver = new ResizeObserver(() => {
|
|
641
|
+
this.syncTimingPanelDisclosureState();
|
|
557
642
|
this.invalidateCameraSafeArea();
|
|
558
643
|
this.syncRendererToCurrentLayout({ render: true });
|
|
559
644
|
});
|
|
560
645
|
if (this.canvasHost) this.layoutResizeObserver.observe(this.canvasHost);
|
|
561
|
-
|
|
646
|
+
resolveNodes(this.readouts.timingTowers, this.readouts.timingTower)
|
|
647
|
+
.forEach((timingTower) => this.layoutResizeObserver.observe(timingTower));
|
|
562
648
|
}
|
|
563
649
|
|
|
564
650
|
invalidateCameraSafeArea() {
|
|
@@ -589,6 +675,7 @@ export class F1SimulatorApp {
|
|
|
589
675
|
renderExpertFrame(snapshot = this.sim?.snapshot(), { forceDomUpdate = false, observation } = {}) {
|
|
590
676
|
if (!snapshot) return;
|
|
591
677
|
const now = performance.now();
|
|
678
|
+
this.sampleFps(now);
|
|
592
679
|
const renderSnapshot = interpolateRenderSnapshotInto(this.renderSnapshotBuffer, snapshot, 0);
|
|
593
680
|
this.applyCamera(renderSnapshot);
|
|
594
681
|
this.renderDrsTrails(renderSnapshot);
|
|
@@ -687,6 +774,7 @@ export class F1SimulatorApp {
|
|
|
687
774
|
}
|
|
688
775
|
|
|
689
776
|
updateDom(snapshot, { emitLifecycle = true } = {}) {
|
|
777
|
+
snapshot = normalizeRuntimeSnapshot(snapshot, this.totalLaps);
|
|
690
778
|
const leader = snapshot.cars[0];
|
|
691
779
|
const selected = snapshot.cars.find((car) => car.id === this.selectedId) ?? leader;
|
|
692
780
|
const activeDrs = snapshot.cars.filter((car) => car.drsActive).length;
|
|
@@ -694,6 +782,7 @@ export class F1SimulatorApp {
|
|
|
694
782
|
const now = performance.now();
|
|
695
783
|
|
|
696
784
|
if (emitLifecycle) this.emitSnapshotLifecycle(snapshot);
|
|
785
|
+
this.syncThemeContext(snapshot);
|
|
697
786
|
|
|
698
787
|
if (
|
|
699
788
|
this.shouldAutoHideActiveRaceData() &&
|
|
@@ -734,7 +823,7 @@ export class F1SimulatorApp {
|
|
|
734
823
|
this.lastTimingPenaltyKey = timingPenaltyKey;
|
|
735
824
|
this.lastTimingOrderKey = timingOrderKey;
|
|
736
825
|
}
|
|
737
|
-
this.renderTelemetry(selected);
|
|
826
|
+
if (selected) this.renderTelemetry(selected);
|
|
738
827
|
const activeRaceDataCar = this.activeRaceDataId
|
|
739
828
|
? snapshot.cars.find((car) => car.id === this.activeRaceDataId)
|
|
740
829
|
: null;
|
|
@@ -821,7 +910,8 @@ export class F1SimulatorApp {
|
|
|
821
910
|
|
|
822
911
|
renderTiming(cars, raceMode, penalties = []) {
|
|
823
912
|
this.syncTimingGapModeControls();
|
|
824
|
-
this.lastTimingMarkup =
|
|
913
|
+
this.lastTimingMarkup = renderTimingTowers({
|
|
914
|
+
timingLists: this.timingLists,
|
|
825
915
|
timingList: this.timingList,
|
|
826
916
|
cars,
|
|
827
917
|
raceMode,
|
|
@@ -834,16 +924,37 @@ export class F1SimulatorApp {
|
|
|
834
924
|
});
|
|
835
925
|
}
|
|
836
926
|
|
|
837
|
-
|
|
838
|
-
|
|
927
|
+
setTimingGapMode(mode) {
|
|
928
|
+
const nextMode = normalizeTimingGapMode(mode);
|
|
929
|
+
if (this.timingGapMode === nextMode) {
|
|
930
|
+
this.syncTimingGapModeControls();
|
|
931
|
+
return this.timingGapMode;
|
|
932
|
+
}
|
|
933
|
+
this.timingGapMode = nextMode;
|
|
934
|
+
this.options = {
|
|
935
|
+
...this.options,
|
|
936
|
+
ui: {
|
|
937
|
+
...this.options.ui,
|
|
938
|
+
timingGapMode: nextMode,
|
|
939
|
+
},
|
|
940
|
+
};
|
|
941
|
+
this.lastTimingRenderTime = 0;
|
|
942
|
+
this.syncTimingGapModeControls();
|
|
943
|
+
const snapshot = this.sim?.snapshot?.();
|
|
944
|
+
if (snapshot) this.renderTiming(snapshot.cars, snapshot.raceControl.mode, snapshot.penalties ?? []);
|
|
945
|
+
return this.timingGapMode;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
getTimingGapMode() {
|
|
949
|
+
return this.timingGapMode;
|
|
839
950
|
}
|
|
840
951
|
|
|
841
|
-
|
|
842
|
-
return
|
|
952
|
+
toggleTimingGapMode() {
|
|
953
|
+
return this.setTimingGapMode(getNextTimingGapMode(this.timingGapMode));
|
|
843
954
|
}
|
|
844
955
|
|
|
845
|
-
|
|
846
|
-
return
|
|
956
|
+
getTimingOrderKey(cars = []) {
|
|
957
|
+
return getTimingOrderKey(cars, { timingGapMode: this.timingGapMode });
|
|
847
958
|
}
|
|
848
959
|
|
|
849
960
|
getTimingPenaltyKey(penalties = []) {
|
|
@@ -852,18 +963,6 @@ export class F1SimulatorApp {
|
|
|
852
963
|
});
|
|
853
964
|
}
|
|
854
965
|
|
|
855
|
-
formatTimingGap(car) {
|
|
856
|
-
return formatRaceGap(car, this.timingGapMode);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
formatLapGap(laps) {
|
|
860
|
-
return formatLapGap(laps);
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
formatTelemetryGap(car, mode) {
|
|
864
|
-
return formatTelemetryGap(car, mode);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
966
|
syncTimingGapModeControls() {
|
|
868
967
|
syncTimingGapModeControls({
|
|
869
968
|
readouts: this.readouts,
|
|
@@ -886,10 +985,6 @@ export class F1SimulatorApp {
|
|
|
886
985
|
renderLapTelemetry(this.readouts, telemetry);
|
|
887
986
|
}
|
|
888
987
|
|
|
889
|
-
getOverviewFields(driver, mode) {
|
|
890
|
-
return getOverviewFields(driver, mode);
|
|
891
|
-
}
|
|
892
|
-
|
|
893
988
|
renderCarDriverOverview(car) {
|
|
894
989
|
const previousKey = this.lastOverviewRenderKey;
|
|
895
990
|
this.lastOverviewRenderKey = renderCarDriverOverview({
|
|
@@ -1091,6 +1186,20 @@ export class F1SimulatorApp {
|
|
|
1091
1186
|
updateToggleButtons(this.safetyButtons, active);
|
|
1092
1187
|
}
|
|
1093
1188
|
|
|
1189
|
+
syncThemeContext(snapshot = null) {
|
|
1190
|
+
const selectedTeamId = this.getSelectedTeamId(snapshot);
|
|
1191
|
+
const contextKey = selectedTeamId ?? '';
|
|
1192
|
+
if (this.lastThemeContextKey === contextKey) return;
|
|
1193
|
+
this.lastThemeContextKey = contextKey;
|
|
1194
|
+
applyPaddockThemeCssVariables(this.root, this.options.theme, { selectedTeamId });
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
getSelectedTeamId(snapshot = null) {
|
|
1198
|
+
const selectedCar = snapshot?.cars?.find?.((car) => car.id === this.selectedId);
|
|
1199
|
+
const selectedDriver = this.driverById.get(this.selectedId);
|
|
1200
|
+
return selectedCar?.team?.id ?? selectedDriver?.team?.id ?? null;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1094
1203
|
applyExpertOptions(nextOptions = {}) {
|
|
1095
1204
|
this.options = {
|
|
1096
1205
|
...this.options,
|
|
@@ -1112,6 +1221,7 @@ export class F1SimulatorApp {
|
|
|
1112
1221
|
},
|
|
1113
1222
|
},
|
|
1114
1223
|
};
|
|
1224
|
+
this.totalLaps = this.options.totalLaps;
|
|
1115
1225
|
this.assets = this.options.assets;
|
|
1116
1226
|
if (hasOwnOption(nextOptions, 'trackSeed')) {
|
|
1117
1227
|
this.trackSeed = this.options.trackSeed ?? createMountTrackSeed();
|
|
@@ -1120,6 +1230,7 @@ export class F1SimulatorApp {
|
|
|
1120
1230
|
this.drivers = nextOptions.drivers;
|
|
1121
1231
|
this.driverById = new Map(this.drivers.map((driver) => [driver.id, driver]));
|
|
1122
1232
|
this.root.style.setProperty('--paddock-entry-count', String(this.drivers.length));
|
|
1233
|
+
this.lastThemeContextKey = null;
|
|
1123
1234
|
this.createCars();
|
|
1124
1235
|
}
|
|
1125
1236
|
}
|
|
@@ -1137,13 +1248,16 @@ export class F1SimulatorApp {
|
|
|
1137
1248
|
this.raceDataBannerConfig = this.options.ui?.raceDataBanners ?? this.raceDataBannerConfig;
|
|
1138
1249
|
this.penaltyBannerEnabled = Boolean(this.options.ui?.penaltyBanners);
|
|
1139
1250
|
this.timingPenaltyBadgesEnabled = Boolean(this.options.ui?.timingPenaltyBadges);
|
|
1140
|
-
this.
|
|
1141
|
-
|
|
1251
|
+
this.timingGapMode = normalizeTimingGapMode(this.options.ui?.timingGapMode);
|
|
1252
|
+
this.cameraController.driverCamera = Boolean(this.options.ui?.driverCamera);
|
|
1253
|
+
this.root.style.setProperty('--broadcast-panel-surface', formatCssUrl(this.assets.broadcastPanel));
|
|
1254
|
+
this.selectedId = this.drivers[0]?.id ?? null;
|
|
1255
|
+
this.lastThemeContextKey = null;
|
|
1256
|
+
this.syncThemeContext();
|
|
1142
1257
|
this.sim = this.createRaceSimulation();
|
|
1143
1258
|
if (this.expertMode) {
|
|
1144
1259
|
this.expert = createBrowserExpertAdapter(this, this.options.expert);
|
|
1145
1260
|
}
|
|
1146
|
-
this.selectedId = this.drivers[0]?.id ?? null;
|
|
1147
1261
|
this.lastTimingMarkup = null;
|
|
1148
1262
|
this.lastOverviewRenderKey = null;
|
|
1149
1263
|
this.lastFinishClassificationMarkup = null;
|
|
@@ -1218,8 +1332,16 @@ export class F1SimulatorApp {
|
|
|
1218
1332
|
cancelAnimationFrame(this.layoutResizeFrame);
|
|
1219
1333
|
}
|
|
1220
1334
|
this.layoutResizeFrame = null;
|
|
1335
|
+
if (this.runtimeVisibilitySyncFrame && typeof cancelAnimationFrame === 'function') {
|
|
1336
|
+
cancelAnimationFrame(this.runtimeVisibilitySyncFrame);
|
|
1337
|
+
}
|
|
1338
|
+
this.runtimeVisibilitySyncFrame = null;
|
|
1221
1339
|
this.layoutResizeObserver?.disconnect?.();
|
|
1222
1340
|
this.layoutResizeObserver = null;
|
|
1341
|
+
this.layoutSupportCleanup?.();
|
|
1342
|
+
this.layoutSupportCleanup = null;
|
|
1343
|
+
this.raceOverlayClearanceCleanup?.();
|
|
1344
|
+
this.raceOverlayClearanceCleanup = null;
|
|
1223
1345
|
this.visibilityObserver?.disconnect?.();
|
|
1224
1346
|
this.visibilityObserver = null;
|
|
1225
1347
|
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);
|