@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,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createComponentSurfaceMarkup,
|
|
3
|
+
createTelemetrySectorBarsMarkup,
|
|
4
|
+
} from './templateUtils.js';
|
|
2
5
|
|
|
3
6
|
export function createStewardMessageMarkup() {
|
|
4
7
|
return `
|
|
@@ -10,13 +13,13 @@ export function createStewardMessageMarkup() {
|
|
|
10
13
|
`;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
export function createRaceDataPanelMarkup({ ui = {} } = {}) {
|
|
16
|
+
export function createRaceDataPanelMarkup({ ui = {}, standalone = false } = {}) {
|
|
14
17
|
const sizeMode = ui.raceDataBannerSize === 'auto' ? 'auto' : 'custom';
|
|
15
18
|
const telemetryDetail = Boolean(ui.raceDataTelemetryDetail);
|
|
16
19
|
const classNames = ['race-data-panel', `race-data-panel--${sizeMode}`];
|
|
20
|
+
if (standalone) classNames.push('race-data-panel--standalone');
|
|
17
21
|
if (telemetryDetail) classNames.push('race-data-panel--with-telemetry');
|
|
18
|
-
|
|
19
|
-
<div class="${classNames.join(' ')}" data-paddock-component="race-data-panel" data-race-data-panel aria-live="polite">
|
|
22
|
+
const body = `
|
|
20
23
|
<button class="race-data-dismiss" type="button" data-race-data-dismiss aria-label="Close race data pill">x</button>
|
|
21
24
|
<div class="race-data-copy">
|
|
22
25
|
<span class="race-data-kicker" data-race-data-kicker>Project</span>
|
|
@@ -26,44 +29,51 @@ export function createRaceDataPanelMarkup({ ui = {} } = {}) {
|
|
|
26
29
|
${telemetryDetail ? createRaceDataTelemetryMarkup() : ''}
|
|
27
30
|
<strong class="race-data-number" data-race-data-number>--</strong>
|
|
28
31
|
<button class="race-data-link" type="button" data-race-data-open>Open project</button>
|
|
29
|
-
${createLoadingMarkup('Race data')}
|
|
30
|
-
</div>
|
|
31
32
|
`;
|
|
33
|
+
|
|
34
|
+
return createComponentSurfaceMarkup({
|
|
35
|
+
tagName: 'div',
|
|
36
|
+
className: classNames.join(' '),
|
|
37
|
+
componentName: 'race-data-panel',
|
|
38
|
+
ariaLabel: 'Race data panel',
|
|
39
|
+
attributes: 'data-race-data-panel aria-live="polite"',
|
|
40
|
+
body,
|
|
41
|
+
unsupportedLabel: 'Race data panel',
|
|
42
|
+
loadingLabel: 'Race data',
|
|
43
|
+
});
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
function createRaceDataTelemetryMarkup() {
|
|
35
47
|
return `
|
|
36
|
-
<div class="race-data-telemetry" data-race-data-telemetry aria-label="Project
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<span>S${sector}</span>
|
|
42
|
-
<strong data-telemetry-sector-time="${sector}">--</strong>
|
|
43
|
-
</div>
|
|
44
|
-
`).join('')}
|
|
45
|
-
</div>
|
|
48
|
+
<div class="race-data-telemetry" data-race-data-telemetry aria-label="Project telemetry">
|
|
49
|
+
${createTelemetrySectorBarsMarkup({
|
|
50
|
+
wrapperClassName: 'race-data-telemetry__bars',
|
|
51
|
+
barClassName: 'telemetry-sector-bar race-data-sector-bar',
|
|
52
|
+
})}
|
|
46
53
|
</div>
|
|
47
54
|
`;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
export function createTelemetrySectorBannerMarkup() {
|
|
51
|
-
|
|
52
|
-
<section class="telemetry-sector-banner" data-paddock-component="telemetry-sector-banner" data-telemetry-sector-banner aria-label="Broadcast sector telemetry">
|
|
58
|
+
const body = `
|
|
53
59
|
<div class="telemetry-sector-banner__copy">
|
|
54
60
|
<span><b data-selected-code>--</b> sector telemetry</span>
|
|
55
61
|
<strong data-selected-name>Select driver</strong>
|
|
56
62
|
<em data-telemetry-current-sector>S1</em>
|
|
57
63
|
</div>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<span>S${sector}</span>
|
|
62
|
-
<strong data-telemetry-sector-time="${sector}">--</strong>
|
|
63
|
-
</div>
|
|
64
|
-
`).join('')}
|
|
65
|
-
</div>
|
|
66
|
-
${createLoadingMarkup('Sector banner')}
|
|
67
|
-
</section>
|
|
64
|
+
${createTelemetrySectorBarsMarkup({
|
|
65
|
+
wrapperClassName: 'telemetry-sector-banner__bars',
|
|
66
|
+
})}
|
|
68
67
|
`;
|
|
68
|
+
|
|
69
|
+
return createComponentSurfaceMarkup({
|
|
70
|
+
tagName: 'section',
|
|
71
|
+
className: 'telemetry-sector-banner',
|
|
72
|
+
componentName: 'telemetry-sector-banner',
|
|
73
|
+
ariaLabel: 'Broadcast sector telemetry',
|
|
74
|
+
attributes: 'data-telemetry-sector-banner',
|
|
75
|
+
body,
|
|
76
|
+
unsupportedLabel: 'Sector banner',
|
|
77
|
+
loadingLabel: 'Sector banner',
|
|
78
|
+
});
|
|
69
79
|
}
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createComponentSurfaceMarkup } from './templateUtils.js';
|
|
2
2
|
|
|
3
3
|
export function createCameraControlsMarkup({ embedded = false, showSimulationSpeed = false, ui = {} } = {}) {
|
|
4
4
|
const className = embedded ? 'camera-controls' : 'camera-controls camera-controls--external';
|
|
5
5
|
const showSpeedControl = showSimulationSpeed || ui.simulationSpeedControl === true;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const driverCameraControl = ui.driverCamera === true
|
|
7
|
+
? '<button type="button" data-camera-mode="driver" aria-pressed="false">Driver</button>'
|
|
8
|
+
: '';
|
|
9
|
+
const body = `
|
|
8
10
|
<button type="button" data-camera-mode="overview" aria-pressed="false">Overview</button>
|
|
9
11
|
<button type="button" data-camera-mode="leader" aria-pressed="true">Leader</button>
|
|
10
12
|
<button type="button" data-camera-mode="selected" aria-pressed="false">Selected</button>
|
|
13
|
+
${driverCameraControl}
|
|
11
14
|
<button type="button" data-camera-mode="show-all" aria-pressed="false">Show all</button>
|
|
12
15
|
<button type="button" data-camera-mode="pit" aria-pressed="false">Pits</button>
|
|
13
16
|
<button type="button" data-zoom-out aria-label="Zoom out">-</button>
|
|
14
17
|
<button type="button" data-zoom-in aria-label="Zoom in">+</button>
|
|
15
18
|
${showSpeedControl ? '<button type="button" data-simulation-speed aria-label="Simulation speed">1x</button>' : ''}
|
|
16
19
|
<button type="button" data-race-data-banners-muted aria-pressed="false">Mute banners</button>
|
|
17
|
-
${createLoadingMarkup('Camera controls')}
|
|
18
|
-
</div>
|
|
19
20
|
`;
|
|
21
|
+
|
|
22
|
+
return createComponentSurfaceMarkup({
|
|
23
|
+
tagName: 'div',
|
|
24
|
+
className,
|
|
25
|
+
componentName: 'camera-controls',
|
|
26
|
+
ariaLabel: 'Camera controls',
|
|
27
|
+
body,
|
|
28
|
+
unsupportedLabel: 'Camera controls',
|
|
29
|
+
loadingLabel: 'Camera controls',
|
|
30
|
+
});
|
|
20
31
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createComponentSurfaceMarkup, escapeHtml } from './templateUtils.js';
|
|
2
2
|
|
|
3
3
|
export function createCarDriverOverviewMarkup({ assets }) {
|
|
4
4
|
const cells = Array.from({ length: 7 }, (_, index) => `
|
|
@@ -8,8 +8,7 @@ export function createCarDriverOverviewMarkup({ assets }) {
|
|
|
8
8
|
</div>
|
|
9
9
|
`).join('');
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
<section class="car-overview" data-paddock-component="car-driver-overview" aria-label="Selected car and driver overview">
|
|
11
|
+
const body = `
|
|
13
12
|
<div class="car-overview-header">
|
|
14
13
|
<span data-car-overview-title>Car overview</span>
|
|
15
14
|
<strong data-car-overview-code>---</strong>
|
|
@@ -27,7 +26,15 @@ export function createCarDriverOverviewMarkup({ assets }) {
|
|
|
27
26
|
<span class="car-overview-core-stat" data-car-overview-core-stat>Car</span>
|
|
28
27
|
</div>
|
|
29
28
|
</div>
|
|
30
|
-
${createLoadingMarkup('Car and driver overview')}
|
|
31
|
-
</section>
|
|
32
29
|
`;
|
|
30
|
+
|
|
31
|
+
return createComponentSurfaceMarkup({
|
|
32
|
+
tagName: 'section',
|
|
33
|
+
className: 'car-overview',
|
|
34
|
+
componentName: 'car-driver-overview',
|
|
35
|
+
ariaLabel: 'Selected car and driver overview',
|
|
36
|
+
body,
|
|
37
|
+
unsupportedLabel: 'Car and driver overview',
|
|
38
|
+
loadingLabel: 'Car and driver overview',
|
|
39
|
+
});
|
|
33
40
|
}
|
|
@@ -5,7 +5,14 @@ import {
|
|
|
5
5
|
} from './bannerTemplates.js';
|
|
6
6
|
import { createCameraControlsMarkup } from './cameraControlsTemplate.js';
|
|
7
7
|
import { createTimingTowerMarkup } from './timingTowerTemplate.js';
|
|
8
|
-
import {
|
|
8
|
+
import { createComponentSurfaceMarkup } from './templateUtils.js';
|
|
9
|
+
|
|
10
|
+
let timingPanelIdSequence = 0;
|
|
11
|
+
|
|
12
|
+
function createTimingPanelId() {
|
|
13
|
+
timingPanelIdSequence += 1;
|
|
14
|
+
return `paddock-timing-panel-${timingPanelIdSequence}`;
|
|
15
|
+
}
|
|
9
16
|
|
|
10
17
|
export function createRaceCanvasMarkup({
|
|
11
18
|
includeRaceDataPanel = false,
|
|
@@ -17,27 +24,35 @@ export function createRaceCanvasMarkup({
|
|
|
17
24
|
physicsMode = 'arcade',
|
|
18
25
|
ui = {},
|
|
19
26
|
debug = {},
|
|
27
|
+
responsiveNarrowLayout,
|
|
20
28
|
} = {}) {
|
|
21
29
|
const showFps = ui.showFps !== false;
|
|
22
30
|
const showEmbeddedCameraControls = ui.cameraControls === 'embedded';
|
|
31
|
+
const enableResponsiveNarrowLayout = responsiveNarrowLayout ?? ui.responsiveNarrowLayout ?? true;
|
|
23
32
|
const timingFit = (timingTowerVerticalFit ?? ui.timingTowerVerticalFit) === 'scroll'
|
|
24
33
|
? 'scroll'
|
|
25
34
|
: 'expand-race-view';
|
|
26
35
|
const classNames = ['sim-canvas-panel'];
|
|
27
36
|
if (includeTimingTower) {
|
|
28
37
|
classNames.push('sim-canvas-panel--with-timing-tower', `sim-canvas-panel--timing-${timingFit}`);
|
|
38
|
+
if (enableResponsiveNarrowLayout) classNames.push('sim-canvas-panel--responsive-narrow');
|
|
29
39
|
}
|
|
40
|
+
const timingPanelId = includeTimingTower ? createTimingPanelId() : '';
|
|
30
41
|
const showPhysicsModeIndicator = debug.physicsModeIndicator === true;
|
|
31
|
-
const physicsModeLabel = physicsMode === '
|
|
32
|
-
const physicsModeClass = physicsMode === '
|
|
42
|
+
const physicsModeLabel = physicsMode === 'advanced' ? 'Advanced physics mode' : 'Arcade physics mode';
|
|
43
|
+
const physicsModeClass = physicsMode === 'advanced' ? 'advanced' : 'arcade';
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
<section class="${classNames.join(' ')}" data-paddock-component="race-canvas" aria-label="Track view">
|
|
45
|
+
const body = `
|
|
36
46
|
<div class="track-canvas" data-track-canvas></div>
|
|
37
47
|
${showPhysicsModeIndicator ? `
|
|
38
48
|
<div class="physics-mode-indicator physics-mode-indicator--${physicsModeClass}" data-physics-mode-indicator aria-label="${physicsModeLabel}" title="${physicsModeLabel}"></div>
|
|
39
49
|
` : ''}
|
|
40
|
-
${includeTimingTower ?
|
|
50
|
+
${includeTimingTower ? `
|
|
51
|
+
<button class="timing-panel-toggle" type="button" data-timing-panel-toggle aria-expanded="false" aria-controls="${timingPanelId}">
|
|
52
|
+
Timing
|
|
53
|
+
</button>
|
|
54
|
+
${createTimingTowerMarkup({ totalLaps, assets, id: timingPanelId, ui })}
|
|
55
|
+
` : ''}
|
|
41
56
|
${showFps ? `
|
|
42
57
|
<div class="fps-counter" aria-label="Frames per second">
|
|
43
58
|
<span>FPS</span>
|
|
@@ -63,7 +78,15 @@ export function createRaceCanvasMarkup({
|
|
|
63
78
|
<strong data-race-finish-winner>--</strong>
|
|
64
79
|
<ol data-race-finish-classification></ol>
|
|
65
80
|
</div>
|
|
66
|
-
${createLoadingMarkup('Race view')}
|
|
67
|
-
</section>
|
|
68
81
|
`;
|
|
82
|
+
|
|
83
|
+
return createComponentSurfaceMarkup({
|
|
84
|
+
tagName: 'section',
|
|
85
|
+
className: classNames.join(' '),
|
|
86
|
+
componentName: 'race-canvas',
|
|
87
|
+
ariaLabel: 'Track view',
|
|
88
|
+
body,
|
|
89
|
+
unsupportedLabel: 'Race view',
|
|
90
|
+
loadingLabel: 'Race view',
|
|
91
|
+
});
|
|
69
92
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createComponentSurfaceMarkup, escapeHtml } from './templateUtils.js';
|
|
2
2
|
|
|
3
3
|
function buttonHiddenAttribute(isVisible) {
|
|
4
4
|
return isVisible ? '' : ' hidden';
|
|
@@ -11,8 +11,7 @@ export function createRaceControlsMarkup({
|
|
|
11
11
|
backLinkLabel,
|
|
12
12
|
showBackLink,
|
|
13
13
|
}) {
|
|
14
|
-
|
|
15
|
-
<header class="sim-topbar" data-paddock-component="race-controls">
|
|
14
|
+
const body = `
|
|
16
15
|
<a class="sim-backlink" href="${escapeHtml(backLinkHref)}"${buttonHiddenAttribute(showBackLink)}>${escapeHtml(backLinkLabel)}</a>
|
|
17
16
|
<div class="sim-title-block">
|
|
18
17
|
<p class="sim-kicker">${escapeHtml(kicker)}</p>
|
|
@@ -22,15 +21,33 @@ export function createRaceControlsMarkup({
|
|
|
22
21
|
${createSafetyCarControlMarkup({ compact: true })}
|
|
23
22
|
<button class="sim-control" type="button" data-restart-race>Restart</button>
|
|
24
23
|
</div>
|
|
25
|
-
${createLoadingMarkup('Race controls')}
|
|
26
|
-
</header>
|
|
27
24
|
`;
|
|
25
|
+
|
|
26
|
+
return createComponentSurfaceMarkup({
|
|
27
|
+
tagName: 'header',
|
|
28
|
+
className: 'sim-topbar',
|
|
29
|
+
componentName: 'race-controls',
|
|
30
|
+
ariaLabel: 'Race controls',
|
|
31
|
+
body,
|
|
32
|
+
unsupportedLabel: 'Race controls',
|
|
33
|
+
loadingLabel: 'Race controls',
|
|
34
|
+
});
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export function createSafetyCarControlMarkup({ compact = false } = {}) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
38
|
+
if (compact) {
|
|
39
|
+
return '<button class="sim-control sim-control--safety" type="button" data-safety-car aria-pressed="false">Safety Car</button>';
|
|
40
|
+
}
|
|
41
|
+
const body = `
|
|
42
|
+
<button class="sim-control sim-control--safety" type="button" data-safety-car aria-pressed="false">Safety Car</button>
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
return createComponentSurfaceMarkup({
|
|
46
|
+
tagName: 'div',
|
|
47
|
+
className: 'standalone-control',
|
|
48
|
+
componentName: 'safety-car-control',
|
|
49
|
+
ariaLabel: 'Safety car control',
|
|
50
|
+
body,
|
|
51
|
+
unsupportedLabel: 'Safety car control',
|
|
52
|
+
});
|
|
36
53
|
}
|
package/src/ui/shellTemplate.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createTelemetryPanelMarkup,
|
|
6
6
|
createTimingTowerMarkup,
|
|
7
7
|
} from './componentTemplates.js';
|
|
8
|
+
import { createUnsupportedSizeMarkup } from './templateUtils.js';
|
|
8
9
|
|
|
9
10
|
export function createF1SimulatorShell({
|
|
10
11
|
title,
|
|
@@ -34,11 +35,12 @@ export function createF1SimulatorShell({
|
|
|
34
35
|
${createRaceControlsMarkup({ title, kicker, backLinkHref, backLinkLabel, showBackLink })}
|
|
35
36
|
${cameraControls}
|
|
36
37
|
<div class="sim-grid">
|
|
37
|
-
${createTimingTowerMarkup({ totalLaps, assets })}
|
|
38
|
+
${createTimingTowerMarkup({ totalLaps, assets, ui })}
|
|
38
39
|
${createRaceCanvasMarkup({ includeRaceDataPanel: ui.showRaceDataPanel !== false, assets, totalLaps, physicsMode, ui })}
|
|
39
40
|
${createTelemetryPanelMarkup({ assets, ui })}
|
|
40
41
|
</div>
|
|
41
42
|
</section>
|
|
43
|
+
${createUnsupportedSizeMarkup('F1 simulator')}
|
|
42
44
|
</main>
|
|
43
45
|
`;
|
|
44
46
|
}
|
|
@@ -2,6 +2,7 @@ import { createCameraControlsMarkup } from './cameraControlsTemplate.js';
|
|
|
2
2
|
import { createRaceCanvasMarkup } from './raceCanvasTemplate.js';
|
|
3
3
|
import { createSafetyCarControlMarkup } from './raceControlsTemplate.js';
|
|
4
4
|
import { createTelemetryPanelMarkup } from './telemetryTemplates.js';
|
|
5
|
+
import { createComponentSurfaceMarkup } from './templateUtils.js';
|
|
5
6
|
|
|
6
7
|
let telemetryDrawerIdSequence = 0;
|
|
7
8
|
|
|
@@ -14,8 +15,11 @@ export function createRaceTelemetryDrawerMarkup(options, {
|
|
|
14
15
|
timingTowerVerticalFit,
|
|
15
16
|
drawerInitiallyOpen = false,
|
|
16
17
|
raceDataTelemetryDetail = options.ui?.raceDataTelemetryDetail,
|
|
18
|
+
responsiveNarrowLayout,
|
|
17
19
|
} = {}) {
|
|
18
20
|
const openClass = drawerInitiallyOpen ? ' is-telemetry-open' : '';
|
|
21
|
+
const enableResponsiveNarrowLayout = responsiveNarrowLayout ?? options.ui?.responsiveNarrowLayout ?? true;
|
|
22
|
+
const responsiveClass = enableResponsiveNarrowLayout ? ' race-telemetry-drawer--responsive-narrow' : '';
|
|
19
23
|
const drawerId = createTelemetryDrawerId();
|
|
20
24
|
const drawerOptions = {
|
|
21
25
|
...options,
|
|
@@ -23,17 +27,17 @@ export function createRaceTelemetryDrawerMarkup(options, {
|
|
|
23
27
|
...(options.ui ?? {}),
|
|
24
28
|
cameraControls: false,
|
|
25
29
|
raceDataTelemetryDetail: Boolean(raceDataTelemetryDetail),
|
|
30
|
+
responsiveNarrowLayout: enableResponsiveNarrowLayout,
|
|
26
31
|
},
|
|
27
32
|
};
|
|
28
33
|
const showCameraControls = options.ui?.cameraControls !== false;
|
|
29
|
-
|
|
30
|
-
<section class="race-telemetry-drawer${openClass}" data-paddock-component="race-telemetry-drawer" data-race-telemetry-drawer aria-label="Race view with telemetry drawer">
|
|
34
|
+
const body = `
|
|
31
35
|
<div class="race-telemetry-drawer__toolbar" aria-label="Race workbench controls">
|
|
32
36
|
${showCameraControls ? createCameraControlsMarkup({ showSimulationSpeed: true, ui: options.ui }) : ''}
|
|
33
37
|
<div class="race-telemetry-drawer__controls">
|
|
34
38
|
${createSafetyCarControlMarkup({ compact: true })}
|
|
35
|
-
<button class="telemetry-drawer-toggle" type="button" data-telemetry-drawer-toggle aria-expanded="${drawerInitiallyOpen ? 'true' : 'false'}" aria-controls="${drawerId}">
|
|
36
|
-
${drawerInitiallyOpen ? 'Close
|
|
39
|
+
<button class="telemetry-drawer-toggle" type="button" data-telemetry-drawer-toggle aria-expanded="${drawerInitiallyOpen ? 'true' : 'false'}" aria-controls="${drawerId}" aria-label="${drawerInitiallyOpen ? 'Close telemetry' : 'Open telemetry'}">
|
|
40
|
+
${drawerInitiallyOpen ? 'Close' : 'Telemetry'}
|
|
37
41
|
</button>
|
|
38
42
|
</div>
|
|
39
43
|
</div>
|
|
@@ -44,12 +48,21 @@ export function createRaceTelemetryDrawerMarkup(options, {
|
|
|
44
48
|
includeTimingTower: true,
|
|
45
49
|
timingTowerVerticalFit,
|
|
46
50
|
})}
|
|
51
|
+
<aside id="${drawerId}" class="telemetry-drawer" data-telemetry-drawer aria-label="Telemetry drawer" aria-hidden="${drawerInitiallyOpen ? 'false' : 'true'}"${drawerInitiallyOpen ? '' : ' inert'}>
|
|
52
|
+
<div class="telemetry-drawer__content">
|
|
53
|
+
${createTelemetryPanelMarkup(options, { includeOverview: false })}
|
|
54
|
+
</div>
|
|
55
|
+
</aside>
|
|
47
56
|
</div>
|
|
48
|
-
<aside id="${drawerId}" class="telemetry-drawer" data-telemetry-drawer aria-label="Telemetry drawer" aria-hidden="${drawerInitiallyOpen ? 'false' : 'true'}"${drawerInitiallyOpen ? '' : ' inert'}>
|
|
49
|
-
<div class="telemetry-drawer__content">
|
|
50
|
-
${createTelemetryPanelMarkup(options, { includeOverview: false })}
|
|
51
|
-
</div>
|
|
52
|
-
</aside>
|
|
53
|
-
</section>
|
|
54
57
|
`;
|
|
58
|
+
|
|
59
|
+
return createComponentSurfaceMarkup({
|
|
60
|
+
tagName: 'section',
|
|
61
|
+
className: `race-telemetry-drawer${responsiveClass}${openClass}`,
|
|
62
|
+
componentName: 'race-telemetry-drawer',
|
|
63
|
+
ariaLabel: 'Race view with telemetry drawer',
|
|
64
|
+
attributes: 'data-race-telemetry-drawer',
|
|
65
|
+
body,
|
|
66
|
+
unsupportedLabel: 'Race telemetry drawer',
|
|
67
|
+
});
|
|
55
68
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createCarDriverOverviewMarkup } from './carOverviewTemplate.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createComponentSurfaceMarkup,
|
|
4
|
+
createTelemetrySectorBarsMarkup,
|
|
5
|
+
} from './templateUtils.js';
|
|
3
6
|
|
|
4
7
|
function getTelemetryModules(ui = {}) {
|
|
5
8
|
const defaults = {
|
|
@@ -26,8 +29,14 @@ function getTelemetryModuleClass(componentName) {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export function createTelemetryCoreMarkup() {
|
|
29
|
-
return
|
|
30
|
-
|
|
32
|
+
return createComponentSurfaceMarkup({
|
|
33
|
+
tagName: 'section',
|
|
34
|
+
className: getTelemetryModuleClass('core'),
|
|
35
|
+
componentName: 'telemetry-core',
|
|
36
|
+
ariaLabel: 'Selected car core telemetry',
|
|
37
|
+
unsupportedLabel: 'Core telemetry',
|
|
38
|
+
loadingLabel: 'Core telemetry',
|
|
39
|
+
body: `
|
|
31
40
|
<div class="telemetry-header">
|
|
32
41
|
<span data-selected-code>---</span>
|
|
33
42
|
<strong data-selected-name>Select car</strong>
|
|
@@ -46,34 +55,40 @@ export function createTelemetryCoreMarkup() {
|
|
|
46
55
|
<div><dt>Interval</dt><dd data-telemetry-gap>--</dd></div>
|
|
47
56
|
<div><dt>Leader</dt><dd data-telemetry-leader-gap>--</dd></div>
|
|
48
57
|
</dl>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`;
|
|
58
|
+
`,
|
|
59
|
+
});
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
export function createTelemetrySectorsMarkup() {
|
|
55
|
-
return
|
|
56
|
-
|
|
63
|
+
return createComponentSurfaceMarkup({
|
|
64
|
+
tagName: 'section',
|
|
65
|
+
className: `${getTelemetryModuleClass('sectors')} telemetry-sector-strip`,
|
|
66
|
+
componentName: 'telemetry-sectors',
|
|
67
|
+
ariaLabel: 'Sector progress',
|
|
68
|
+
attributes: 'data-telemetry-sector-strip',
|
|
69
|
+
unsupportedLabel: 'Sector telemetry',
|
|
70
|
+
loadingLabel: 'Sector telemetry',
|
|
71
|
+
body: `
|
|
57
72
|
<div class="telemetry-module-header">
|
|
58
73
|
<span>Sector map</span>
|
|
59
74
|
<strong data-telemetry-current-sector>S1</strong>
|
|
60
75
|
</div>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</div>
|
|
67
|
-
`).join('')}
|
|
68
|
-
</div>
|
|
69
|
-
${createLoadingMarkup('Sector telemetry')}
|
|
70
|
-
</section>
|
|
71
|
-
`;
|
|
76
|
+
${createTelemetrySectorBarsMarkup({
|
|
77
|
+
wrapperClassName: 'telemetry-sector-bars',
|
|
78
|
+
})}
|
|
79
|
+
`,
|
|
80
|
+
});
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
export function createTelemetryLapTimesMarkup() {
|
|
75
|
-
return
|
|
76
|
-
|
|
84
|
+
return createComponentSurfaceMarkup({
|
|
85
|
+
tagName: 'section',
|
|
86
|
+
className: `${getTelemetryModuleClass('lap-times')} telemetry-lap-module`,
|
|
87
|
+
componentName: 'telemetry-lap-times',
|
|
88
|
+
ariaLabel: 'Lap timing',
|
|
89
|
+
unsupportedLabel: 'Lap telemetry',
|
|
90
|
+
loadingLabel: 'Lap telemetry',
|
|
91
|
+
body: `
|
|
77
92
|
<div class="telemetry-module-header">
|
|
78
93
|
<span>Lap timing</span>
|
|
79
94
|
<strong data-telemetry-completed-laps>0 laps</strong>
|
|
@@ -85,14 +100,19 @@ export function createTelemetryLapTimesMarkup() {
|
|
|
85
100
|
<tr><th scope="row">Best</th><td data-telemetry-best-lap-time>--</td></tr>
|
|
86
101
|
</tbody>
|
|
87
102
|
</table>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`;
|
|
103
|
+
`,
|
|
104
|
+
});
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
export function createTelemetrySectorTimesMarkup() {
|
|
94
|
-
return
|
|
95
|
-
|
|
108
|
+
return createComponentSurfaceMarkup({
|
|
109
|
+
tagName: 'section',
|
|
110
|
+
className: `${getTelemetryModuleClass('sector-times')} telemetry-sector-table-module`,
|
|
111
|
+
componentName: 'telemetry-sector-times',
|
|
112
|
+
ariaLabel: 'Sector timing table',
|
|
113
|
+
unsupportedLabel: 'Sector table',
|
|
114
|
+
loadingLabel: 'Sector table',
|
|
115
|
+
body: `
|
|
96
116
|
<div class="telemetry-module-header">
|
|
97
117
|
<span>Sector timing</span>
|
|
98
118
|
<strong>Last / Best</strong>
|
|
@@ -111,9 +131,8 @@ export function createTelemetrySectorTimesMarkup() {
|
|
|
111
131
|
`).join('')}
|
|
112
132
|
</tbody>
|
|
113
133
|
</table>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
`;
|
|
134
|
+
`,
|
|
135
|
+
});
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
function createTelemetryComponentMarkup(options, modules = getTelemetryModules(options.ui)) {
|
|
@@ -127,10 +146,17 @@ function createTelemetryComponentMarkup(options, modules = getTelemetryModules(o
|
|
|
127
146
|
|
|
128
147
|
export function createTelemetryPanelMarkup(options, { includeOverview = options.ui?.telemetryIncludesOverview !== false } = {}) {
|
|
129
148
|
const modules = getTelemetryModules(options.ui);
|
|
130
|
-
|
|
131
|
-
<aside class="telemetry-stack" data-paddock-component="telemetry-stack" aria-label="Selected car telemetry stack">
|
|
149
|
+
const body = `
|
|
132
150
|
${createTelemetryComponentMarkup(options, modules)}
|
|
133
151
|
${includeOverview ? createCarDriverOverviewMarkup(options) : ''}
|
|
134
|
-
</aside>
|
|
135
152
|
`;
|
|
153
|
+
|
|
154
|
+
return createComponentSurfaceMarkup({
|
|
155
|
+
tagName: 'aside',
|
|
156
|
+
className: 'telemetry-stack',
|
|
157
|
+
componentName: 'telemetry-stack',
|
|
158
|
+
ariaLabel: 'Selected car telemetry stack',
|
|
159
|
+
body,
|
|
160
|
+
unsupportedLabel: 'Telemetry stack',
|
|
161
|
+
});
|
|
136
162
|
}
|
package/src/ui/templateUtils.js
CHANGED
|
@@ -21,3 +21,51 @@ export function createLoadingMarkup(label = 'Loading') {
|
|
|
21
21
|
</div>
|
|
22
22
|
`;
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
export function createUnsupportedSizeMarkup(label = 'PaddockJS component') {
|
|
26
|
+
return `
|
|
27
|
+
<div class="paddock-unsupported-size" data-paddock-unsupported-size role="status" aria-live="polite">
|
|
28
|
+
<strong>Unsupported size</strong>
|
|
29
|
+
<span>${escapeHtml(label)} needs more inline space or height to remain usable.</span>
|
|
30
|
+
</div>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const COMPONENT_SURFACE_TAGS = new Set(['section', 'aside', 'div', 'header']);
|
|
35
|
+
|
|
36
|
+
export function createComponentSurfaceMarkup({
|
|
37
|
+
tagName = 'section',
|
|
38
|
+
className,
|
|
39
|
+
componentName,
|
|
40
|
+
ariaLabel,
|
|
41
|
+
attributes = '',
|
|
42
|
+
body = '',
|
|
43
|
+
unsupportedLabel,
|
|
44
|
+
loadingLabel,
|
|
45
|
+
} = {}) {
|
|
46
|
+
const tag = COMPONENT_SURFACE_TAGS.has(tagName) ? tagName : 'section';
|
|
47
|
+
const extraAttributes = attributes ? ` ${attributes}` : '';
|
|
48
|
+
return `
|
|
49
|
+
<${tag} class="${escapeHtml(className)}" data-paddock-component="${escapeHtml(componentName)}"${extraAttributes} aria-label="${escapeHtml(ariaLabel)}">
|
|
50
|
+
${body}
|
|
51
|
+
${unsupportedLabel ? createUnsupportedSizeMarkup(unsupportedLabel) : ''}
|
|
52
|
+
${loadingLabel ? createLoadingMarkup(loadingLabel) : ''}
|
|
53
|
+
</${tag}>
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createTelemetrySectorBarsMarkup({
|
|
58
|
+
wrapperClassName = 'telemetry-sector-bars',
|
|
59
|
+
barClassName = 'telemetry-sector-bar',
|
|
60
|
+
} = {}) {
|
|
61
|
+
return `
|
|
62
|
+
<div class="${escapeHtml(wrapperClassName)}">
|
|
63
|
+
${[1, 2, 3].map((sector) => `
|
|
64
|
+
<div class="${escapeHtml(barClassName)}" data-telemetry-sector-bar="${sector}" style="--sector-fill: 0%">
|
|
65
|
+
<span>S${sector}</span>
|
|
66
|
+
<strong data-telemetry-sector-time="${sector}">--</strong>
|
|
67
|
+
</div>
|
|
68
|
+
`).join('')}
|
|
69
|
+
</div>
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import { getTimingGapModeLabel, normalizeTimingGapMode } from '../config/timingGapMode.js';
|
|
1
2
|
import { createRaceControlStatusBannerMarkup } from './raceControlStatusBanner.js';
|
|
2
|
-
import {
|
|
3
|
+
import { createComponentSurfaceMarkup, escapeHtml } from './templateUtils.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
function normalizeTimingEntryVerticalPadding(value) {
|
|
6
|
+
const numericValue = Number(value);
|
|
7
|
+
return Number.isFinite(numericValue) && numericValue >= 0 ? numericValue : 5;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createTimingTowerMarkup({ totalLaps, assets, id, ui = {} }) {
|
|
11
|
+
const idAttribute = id ? `id="${escapeHtml(id)}" ` : '';
|
|
12
|
+
const timingGapMode = normalizeTimingGapMode(ui.timingGapMode);
|
|
13
|
+
const timingGapLabel = getTimingGapModeLabel(timingGapMode);
|
|
14
|
+
const showGapToggle = ui.timingGapModeToggle !== false;
|
|
15
|
+
const timingEntryVerticalPadding = normalizeTimingEntryVerticalPadding(ui.timingEntryVerticalPadding);
|
|
16
|
+
const body = `
|
|
7
17
|
<div class="broadcast-tower-frame">
|
|
8
18
|
<div class="broadcast-brand">
|
|
9
19
|
<img class="broadcast-f1-logo" src="${escapeHtml(assets.f1Logo)}" alt="F1" />
|
|
@@ -14,21 +24,28 @@ export function createTimingTowerMarkup({ totalLaps, assets }) {
|
|
|
14
24
|
<span>/</span>
|
|
15
25
|
<span data-tower-total-laps>${escapeHtml(totalLaps)}</span>
|
|
16
26
|
</div>
|
|
17
|
-
<div class="broadcast-gap-mode" role="group" aria-label="Timing gap mode">
|
|
18
|
-
<button type="button" data-timing-gap-mode="interval" aria-pressed="true">Int</button>
|
|
19
|
-
<button type="button" data-timing-gap-mode="leader" aria-pressed="false">Gap</button>
|
|
20
|
-
</div>
|
|
21
27
|
${createRaceControlStatusBannerMarkup()}
|
|
22
|
-
<div class="broadcast-column-head"
|
|
28
|
+
<div class="broadcast-column-head">
|
|
23
29
|
<span>Pos</span>
|
|
24
30
|
<span>Team</span>
|
|
25
31
|
<span>Project</span>
|
|
26
|
-
|
|
32
|
+
${showGapToggle
|
|
33
|
+
? `<button class="broadcast-gap-mode-toggle" type="button" data-timing-gap-toggle data-timing-gap-label aria-pressed="${timingGapMode === 'leader'}" aria-label="Timing gap mode ${escapeHtml(timingGapLabel)}">${escapeHtml(timingGapLabel)}</button>`
|
|
34
|
+
: `<span data-timing-gap-label>${escapeHtml(timingGapLabel)}</span>`}
|
|
27
35
|
<span>Tyre</span>
|
|
28
36
|
</div>
|
|
29
37
|
<ol class="timing-list" data-timing-list></ol>
|
|
30
38
|
</div>
|
|
31
|
-
${createLoadingMarkup('Timing tower')}
|
|
32
|
-
</aside>
|
|
33
39
|
`;
|
|
40
|
+
|
|
41
|
+
return createComponentSurfaceMarkup({
|
|
42
|
+
tagName: 'aside',
|
|
43
|
+
className: 'sim-timing broadcast-tower',
|
|
44
|
+
componentName: 'timing-tower',
|
|
45
|
+
ariaLabel: 'Timing tower',
|
|
46
|
+
attributes: `${idAttribute}data-timing-tower style="--timing-entry-extra-block-padding: ${timingEntryVerticalPadding}px"`,
|
|
47
|
+
body,
|
|
48
|
+
unsupportedLabel: 'Timing tower',
|
|
49
|
+
loadingLabel: 'Timing tower',
|
|
50
|
+
});
|
|
34
51
|
}
|