@inventure71/paddockjs 2.0.1 → 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 +31 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +65 -15
- package/docs/architecture.md +17 -13
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +2 -2
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +96 -21
- package/docs/index.md +2 -1
- package/docs/learnings.md +6 -6
- package/docs/rules.md +7 -7
- package/docs/sense_contract.md +1 -1
- package/docs/system_specs.md +25 -18
- package/docs/training.md +13 -12
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +74 -4
- package/src/app/F1SimulatorApp.js +191 -28
- 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/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 +17 -3
- package/src/environment/index.d.ts +1 -2
- package/src/environment/options.js +1 -1
- package/src/environment/runtime.js +3 -11
- package/src/environment/scenarios.js +1 -1
- package/src/environment/sensors/surfaceRays.js +41 -33
- package/src/environment/sensors/trackRays.js +44 -49
- package/src/index.d.ts +110 -9
- package/src/index.js +25 -1
- 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/pitKinematics.js +2 -2
- package/src/simulation/pit/pitRouting.js +2 -4
- package/src/simulation/race/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/safetyCar.js +1 -1
- package/src/simulation/race/stalledDnf.js +1 -1
- 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/trackQueryIndex.js +180 -135
- package/src/simulation/track/trackStatePolicy.js +0 -1
- package/src/simulation/vehicle/contactResolution.js +3 -3
- package/src/simulation/vehicle/vehicleKinematics.js +2 -2
- package/src/simulation/vehicle/vehiclePhysics.js +3 -3
- package/src/simulation/vehicle/vehicleSnapshots.js +12 -1
- package/src/simulation/vehicle/vehicleState.js +1 -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';
|
|
@@ -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,10 @@ 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.applyCamera(this.sim.snapshotRender?.() ?? this.sim.snapshot());
|
|
305
|
+
};
|
|
254
306
|
window.addEventListener('resize', this.resizeHandler, { signal: this.abortController.signal });
|
|
255
307
|
this.observeLayoutResize();
|
|
256
308
|
if (this.expertMode) {
|
|
@@ -304,7 +356,6 @@ export class F1SimulatorApp {
|
|
|
304
356
|
rules: options.rules,
|
|
305
357
|
participantInteractions: options.participantInteractions,
|
|
306
358
|
replayGhosts: options.replayGhosts,
|
|
307
|
-
trackQueryIndex: options.trackQueryIndex,
|
|
308
359
|
warmup: normalizeWarmupOptions(options.warmup, 'browser'),
|
|
309
360
|
});
|
|
310
361
|
}
|
|
@@ -383,30 +434,42 @@ export class F1SimulatorApp {
|
|
|
383
434
|
|
|
384
435
|
this.timingGapModeButtons.forEach((button) => {
|
|
385
436
|
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 ?? []);
|
|
437
|
+
this.toggleTimingGapMode();
|
|
390
438
|
}, eventOptions);
|
|
391
439
|
});
|
|
392
440
|
|
|
393
|
-
this.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
+
});
|
|
398
448
|
|
|
399
|
-
this.
|
|
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
|
+
});
|
|
455
|
+
|
|
456
|
+
const raceDataOpenButtons = uniqueNodes(this.readouts.raceDataOpens?.length
|
|
457
|
+
? this.readouts.raceDataOpens
|
|
458
|
+
: [this.openButton]);
|
|
459
|
+
raceDataOpenButtons.forEach((button) => button.addEventListener('click', () => {
|
|
400
460
|
const driver = this.driverById.get(this.activeRaceDataId ?? this.selectedId);
|
|
401
461
|
if (!driver) return;
|
|
402
462
|
this.options.onDriverOpen?.(driver);
|
|
403
|
-
}, eventOptions);
|
|
463
|
+
}, eventOptions));
|
|
404
464
|
|
|
405
|
-
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) => {
|
|
406
469
|
event.preventDefault();
|
|
407
470
|
event.stopPropagation();
|
|
408
471
|
this.dismissRaceDataPanel(performance.now());
|
|
409
|
-
}, eventOptions);
|
|
472
|
+
}, eventOptions));
|
|
410
473
|
|
|
411
474
|
this.readouts.telemetryDrawerToggle?.addEventListener('click', () => {
|
|
412
475
|
this.setTelemetryDrawerOpen(!this.telemetryDrawerOpen);
|
|
@@ -466,13 +529,51 @@ export class F1SimulatorApp {
|
|
|
466
529
|
}
|
|
467
530
|
this.readouts.telemetryDrawerToggle?.setAttribute?.('aria-expanded', String(this.telemetryDrawerOpen));
|
|
468
531
|
if (this.readouts.telemetryDrawerToggle) {
|
|
469
|
-
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
|
+
);
|
|
470
537
|
}
|
|
471
538
|
this.invalidateCameraSafeArea();
|
|
472
539
|
this.syncRendererToCurrentLayout({ render: true });
|
|
473
540
|
this.scheduleLayoutResizeSync(380);
|
|
474
541
|
}
|
|
475
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
|
+
|
|
476
577
|
setRaceDataBannersMuted(muted, now = performance.now()) {
|
|
477
578
|
this.raceDataBannersMuted = Boolean(muted);
|
|
478
579
|
if (this.raceDataBannersMuted) {
|
|
@@ -537,11 +638,13 @@ export class F1SimulatorApp {
|
|
|
537
638
|
if (typeof ResizeObserver !== 'function') return;
|
|
538
639
|
this.layoutResizeObserver?.disconnect?.();
|
|
539
640
|
this.layoutResizeObserver = new ResizeObserver(() => {
|
|
641
|
+
this.syncTimingPanelDisclosureState();
|
|
540
642
|
this.invalidateCameraSafeArea();
|
|
541
643
|
this.syncRendererToCurrentLayout({ render: true });
|
|
542
644
|
});
|
|
543
645
|
if (this.canvasHost) this.layoutResizeObserver.observe(this.canvasHost);
|
|
544
|
-
|
|
646
|
+
resolveNodes(this.readouts.timingTowers, this.readouts.timingTower)
|
|
647
|
+
.forEach((timingTower) => this.layoutResizeObserver.observe(timingTower));
|
|
545
648
|
}
|
|
546
649
|
|
|
547
650
|
invalidateCameraSafeArea() {
|
|
@@ -572,6 +675,7 @@ export class F1SimulatorApp {
|
|
|
572
675
|
renderExpertFrame(snapshot = this.sim?.snapshot(), { forceDomUpdate = false, observation } = {}) {
|
|
573
676
|
if (!snapshot) return;
|
|
574
677
|
const now = performance.now();
|
|
678
|
+
this.sampleFps(now);
|
|
575
679
|
const renderSnapshot = interpolateRenderSnapshotInto(this.renderSnapshotBuffer, snapshot, 0);
|
|
576
680
|
this.applyCamera(renderSnapshot);
|
|
577
681
|
this.renderDrsTrails(renderSnapshot);
|
|
@@ -670,6 +774,7 @@ export class F1SimulatorApp {
|
|
|
670
774
|
}
|
|
671
775
|
|
|
672
776
|
updateDom(snapshot, { emitLifecycle = true } = {}) {
|
|
777
|
+
snapshot = normalizeRuntimeSnapshot(snapshot, this.totalLaps);
|
|
673
778
|
const leader = snapshot.cars[0];
|
|
674
779
|
const selected = snapshot.cars.find((car) => car.id === this.selectedId) ?? leader;
|
|
675
780
|
const activeDrs = snapshot.cars.filter((car) => car.drsActive).length;
|
|
@@ -677,6 +782,7 @@ export class F1SimulatorApp {
|
|
|
677
782
|
const now = performance.now();
|
|
678
783
|
|
|
679
784
|
if (emitLifecycle) this.emitSnapshotLifecycle(snapshot);
|
|
785
|
+
this.syncThemeContext(snapshot);
|
|
680
786
|
|
|
681
787
|
if (
|
|
682
788
|
this.shouldAutoHideActiveRaceData() &&
|
|
@@ -717,7 +823,7 @@ export class F1SimulatorApp {
|
|
|
717
823
|
this.lastTimingPenaltyKey = timingPenaltyKey;
|
|
718
824
|
this.lastTimingOrderKey = timingOrderKey;
|
|
719
825
|
}
|
|
720
|
-
this.renderTelemetry(selected);
|
|
826
|
+
if (selected) this.renderTelemetry(selected);
|
|
721
827
|
const activeRaceDataCar = this.activeRaceDataId
|
|
722
828
|
? snapshot.cars.find((car) => car.id === this.activeRaceDataId)
|
|
723
829
|
: null;
|
|
@@ -804,7 +910,8 @@ export class F1SimulatorApp {
|
|
|
804
910
|
|
|
805
911
|
renderTiming(cars, raceMode, penalties = []) {
|
|
806
912
|
this.syncTimingGapModeControls();
|
|
807
|
-
this.lastTimingMarkup =
|
|
913
|
+
this.lastTimingMarkup = renderTimingTowers({
|
|
914
|
+
timingLists: this.timingLists,
|
|
808
915
|
timingList: this.timingList,
|
|
809
916
|
cars,
|
|
810
917
|
raceMode,
|
|
@@ -817,6 +924,35 @@ export class F1SimulatorApp {
|
|
|
817
924
|
});
|
|
818
925
|
}
|
|
819
926
|
|
|
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;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
toggleTimingGapMode() {
|
|
953
|
+
return this.setTimingGapMode(getNextTimingGapMode(this.timingGapMode));
|
|
954
|
+
}
|
|
955
|
+
|
|
820
956
|
getTimingOrderKey(cars = []) {
|
|
821
957
|
return getTimingOrderKey(cars, { timingGapMode: this.timingGapMode });
|
|
822
958
|
}
|
|
@@ -1050,6 +1186,20 @@ export class F1SimulatorApp {
|
|
|
1050
1186
|
updateToggleButtons(this.safetyButtons, active);
|
|
1051
1187
|
}
|
|
1052
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
|
+
|
|
1053
1203
|
applyExpertOptions(nextOptions = {}) {
|
|
1054
1204
|
this.options = {
|
|
1055
1205
|
...this.options,
|
|
@@ -1071,6 +1221,7 @@ export class F1SimulatorApp {
|
|
|
1071
1221
|
},
|
|
1072
1222
|
},
|
|
1073
1223
|
};
|
|
1224
|
+
this.totalLaps = this.options.totalLaps;
|
|
1074
1225
|
this.assets = this.options.assets;
|
|
1075
1226
|
if (hasOwnOption(nextOptions, 'trackSeed')) {
|
|
1076
1227
|
this.trackSeed = this.options.trackSeed ?? createMountTrackSeed();
|
|
@@ -1079,6 +1230,7 @@ export class F1SimulatorApp {
|
|
|
1079
1230
|
this.drivers = nextOptions.drivers;
|
|
1080
1231
|
this.driverById = new Map(this.drivers.map((driver) => [driver.id, driver]));
|
|
1081
1232
|
this.root.style.setProperty('--paddock-entry-count', String(this.drivers.length));
|
|
1233
|
+
this.lastThemeContextKey = null;
|
|
1082
1234
|
this.createCars();
|
|
1083
1235
|
}
|
|
1084
1236
|
}
|
|
@@ -1096,13 +1248,16 @@ export class F1SimulatorApp {
|
|
|
1096
1248
|
this.raceDataBannerConfig = this.options.ui?.raceDataBanners ?? this.raceDataBannerConfig;
|
|
1097
1249
|
this.penaltyBannerEnabled = Boolean(this.options.ui?.penaltyBanners);
|
|
1098
1250
|
this.timingPenaltyBadgesEnabled = Boolean(this.options.ui?.timingPenaltyBadges);
|
|
1099
|
-
this.
|
|
1100
|
-
|
|
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();
|
|
1101
1257
|
this.sim = this.createRaceSimulation();
|
|
1102
1258
|
if (this.expertMode) {
|
|
1103
1259
|
this.expert = createBrowserExpertAdapter(this, this.options.expert);
|
|
1104
1260
|
}
|
|
1105
|
-
this.selectedId = this.drivers[0]?.id ?? null;
|
|
1106
1261
|
this.lastTimingMarkup = null;
|
|
1107
1262
|
this.lastOverviewRenderKey = null;
|
|
1108
1263
|
this.lastFinishClassificationMarkup = null;
|
|
@@ -1177,8 +1332,16 @@ export class F1SimulatorApp {
|
|
|
1177
1332
|
cancelAnimationFrame(this.layoutResizeFrame);
|
|
1178
1333
|
}
|
|
1179
1334
|
this.layoutResizeFrame = null;
|
|
1335
|
+
if (this.runtimeVisibilitySyncFrame && typeof cancelAnimationFrame === 'function') {
|
|
1336
|
+
cancelAnimationFrame(this.runtimeVisibilitySyncFrame);
|
|
1337
|
+
}
|
|
1338
|
+
this.runtimeVisibilitySyncFrame = null;
|
|
1180
1339
|
this.layoutResizeObserver?.disconnect?.();
|
|
1181
1340
|
this.layoutResizeObserver = null;
|
|
1341
|
+
this.layoutSupportCleanup?.();
|
|
1342
|
+
this.layoutSupportCleanup = null;
|
|
1343
|
+
this.raceOverlayClearanceCleanup?.();
|
|
1344
|
+
this.raceOverlayClearanceCleanup = null;
|
|
1182
1345
|
this.visibilityObserver?.disconnect?.();
|
|
1183
1346
|
this.visibilityObserver = null;
|
|
1184
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);
|
|
@@ -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
|
};
|