@inventure71/paddockjs 0.1.0 → 0.2.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 +10 -0
- package/README.md +20 -4
- package/docs/architecture.md +15 -6
- package/docs/concepts.md +5 -1
- package/docs/data_contract.md +85 -3
- package/docs/rules.md +2 -0
- package/docs/system_specs.md +29 -5
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +57 -1
- package/src/app/F1SimulatorApp.js +160 -22
- package/src/app/domBindings.js +36 -11
- package/src/config/defaultOptions.js +25 -0
- package/src/index.d.ts +57 -0
- package/src/index.js +6 -0
- package/src/simulation/raceSimulation.js +205 -0
- package/src/simulation/trackModel.js +21 -0
- package/src/styles.css +451 -2
- package/src/ui/componentTemplates.js +176 -3
|
@@ -7,7 +7,7 @@ import { createRenderSnapshot } from '../rendering/renderSnapshot.js';
|
|
|
7
7
|
import { createRaceSimulation } from '../simulation/raceSimulation.js';
|
|
8
8
|
import { clamp } from '../simulation/simMath.js';
|
|
9
9
|
import { offsetTrackPoint, pointAt, WORLD } from '../simulation/trackModel.js';
|
|
10
|
-
import { querySimulatorDom, setText } from './domBindings.js';
|
|
10
|
+
import { querySimulatorDom, setText, setTextAll } from './domBindings.js';
|
|
11
11
|
|
|
12
12
|
const FIXED_STEP = 1 / 60;
|
|
13
13
|
const TARGET_RENDER_FPS = 60;
|
|
@@ -84,6 +84,23 @@ function getTireClass(tire) {
|
|
|
84
84
|
return String(tire ?? 'M').toLowerCase();
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function formatTelemetryTime(value) {
|
|
88
|
+
if (!Number.isFinite(value)) return '--';
|
|
89
|
+
const clamped = Math.max(0, value);
|
|
90
|
+
if (clamped >= 60) {
|
|
91
|
+
const minutes = Math.floor(clamped / 60);
|
|
92
|
+
const seconds = clamped - minutes * 60;
|
|
93
|
+
return `${minutes}:${seconds.toFixed(3).padStart(6, '0')}`;
|
|
94
|
+
}
|
|
95
|
+
return `${clamped.toFixed(3)}s`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setPerformanceClass(node, status) {
|
|
99
|
+
['overall-best', 'personal-best', 'slower'].forEach((name) => {
|
|
100
|
+
node?.classList?.toggle?.(`is-${name}`, status === name);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
87
104
|
function colorToTint(color) {
|
|
88
105
|
if (TINT_BY_COLOR.has(color)) return TINT_BY_COLOR.get(color);
|
|
89
106
|
const tint = Number.parseInt(String(color ?? '').replace('#', ''), 16);
|
|
@@ -155,6 +172,9 @@ export class F1SimulatorApp {
|
|
|
155
172
|
this.lastDomUpdateTime = 0;
|
|
156
173
|
this.lastTimingRenderTime = 0;
|
|
157
174
|
this.lastTimingRaceMode = null;
|
|
175
|
+
this.lastTimingMarkup = null;
|
|
176
|
+
this.lastOverviewRenderKey = null;
|
|
177
|
+
this.lastFinishClassificationMarkup = null;
|
|
158
178
|
this.runtimeViewportVisible = true;
|
|
159
179
|
this.runtimeDocumentVisible = typeof document === 'undefined' ? true : document.visibilityState !== 'hidden';
|
|
160
180
|
this.runtimeTickerRunning = true;
|
|
@@ -167,6 +187,9 @@ export class F1SimulatorApp {
|
|
|
167
187
|
this.emittedRaceEventKeys = new Set();
|
|
168
188
|
this.raceFinishEmitted = false;
|
|
169
189
|
this.timingGapMode = 'interval';
|
|
190
|
+
this.telemetryDrawerOpen = Boolean(
|
|
191
|
+
this.readouts.telemetryDrawerWorkbench?.classList?.contains?.('is-telemetry-open'),
|
|
192
|
+
);
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
async init() {
|
|
@@ -391,6 +414,31 @@ export class F1SimulatorApp {
|
|
|
391
414
|
if (!driver) return;
|
|
392
415
|
this.options.onDriverOpen?.(driver);
|
|
393
416
|
}, eventOptions);
|
|
417
|
+
|
|
418
|
+
this.readouts.telemetryDrawerToggle?.addEventListener('click', () => {
|
|
419
|
+
this.setTelemetryDrawerOpen(!this.telemetryDrawerOpen);
|
|
420
|
+
}, eventOptions);
|
|
421
|
+
|
|
422
|
+
this.readouts.telemetryDrawerClose?.addEventListener('click', () => {
|
|
423
|
+
this.setTelemetryDrawerOpen(false);
|
|
424
|
+
}, eventOptions);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
setTelemetryDrawerOpen(open) {
|
|
428
|
+
this.telemetryDrawerOpen = Boolean(open);
|
|
429
|
+
this.readouts.telemetryDrawerWorkbench?.classList?.toggle?.('is-telemetry-open', this.telemetryDrawerOpen);
|
|
430
|
+
if (this.telemetryDrawerOpen) {
|
|
431
|
+
this.readouts.telemetryDrawer?.removeAttribute?.('inert');
|
|
432
|
+
this.readouts.telemetryDrawer?.setAttribute?.('aria-hidden', 'false');
|
|
433
|
+
} else {
|
|
434
|
+
this.readouts.telemetryDrawer?.setAttribute?.('inert', '');
|
|
435
|
+
this.readouts.telemetryDrawer?.setAttribute?.('aria-hidden', 'true');
|
|
436
|
+
}
|
|
437
|
+
this.readouts.telemetryDrawerToggle?.setAttribute?.('aria-expanded', String(this.telemetryDrawerOpen));
|
|
438
|
+
if (this.readouts.telemetryDrawerToggle) {
|
|
439
|
+
this.readouts.telemetryDrawerToggle.textContent = this.telemetryDrawerOpen ? 'Close telemetry' : 'Telemetry';
|
|
440
|
+
}
|
|
441
|
+
this.invalidateCameraSafeArea();
|
|
394
442
|
}
|
|
395
443
|
|
|
396
444
|
observeLayoutResize() {
|
|
@@ -637,8 +685,17 @@ export class F1SimulatorApp {
|
|
|
637
685
|
this.cameraSafeAreaCache = { width, safeArea };
|
|
638
686
|
return safeArea;
|
|
639
687
|
}
|
|
640
|
-
const
|
|
641
|
-
|
|
688
|
+
const overlapsCanvasHorizontally = towerRect.right > canvasRect.left && towerRect.left < canvasRect.right;
|
|
689
|
+
const hasVerticalBounds = Number.isFinite(canvasRect.top) &&
|
|
690
|
+
Number.isFinite(canvasRect.bottom) &&
|
|
691
|
+
Number.isFinite(towerRect.top) &&
|
|
692
|
+
Number.isFinite(towerRect.bottom);
|
|
693
|
+
const overlapsCanvasVertically = !hasVerticalBounds ||
|
|
694
|
+
(towerRect.bottom > canvasRect.top && towerRect.top < canvasRect.bottom);
|
|
695
|
+
const canvasWidth = Math.max(1, canvasRect.right - canvasRect.left || width);
|
|
696
|
+
const towerWidth = Math.max(0, towerRect.right - towerRect.left);
|
|
697
|
+
const isSideGutter = towerWidth < canvasWidth * 0.6;
|
|
698
|
+
if (!overlapsCanvasHorizontally || !overlapsCanvasVertically || !isSideGutter) {
|
|
642
699
|
const safeArea = { left: 0, width };
|
|
643
700
|
this.cameraSafeAreaCache = { width, safeArea };
|
|
644
701
|
return safeArea;
|
|
@@ -837,12 +894,16 @@ export class F1SimulatorApp {
|
|
|
837
894
|
|
|
838
895
|
if (this.readouts.finishClassification) {
|
|
839
896
|
const topThree = (snapshot.raceControl.classification ?? []).slice(0, 3);
|
|
840
|
-
|
|
897
|
+
const classificationMarkup = topThree.map((entry) => `
|
|
841
898
|
<li>
|
|
842
899
|
<span>P${escapeHtml(entry.rank)}</span>
|
|
843
900
|
<strong>${escapeHtml(entry.timingCode ?? entry.code ?? entry.id)}</strong>
|
|
844
901
|
</li>
|
|
845
902
|
`).join('');
|
|
903
|
+
if (classificationMarkup !== this.lastFinishClassificationMarkup) {
|
|
904
|
+
this.readouts.finishClassification.innerHTML = classificationMarkup;
|
|
905
|
+
this.lastFinishClassificationMarkup = classificationMarkup;
|
|
906
|
+
}
|
|
846
907
|
}
|
|
847
908
|
}
|
|
848
909
|
|
|
@@ -903,7 +964,7 @@ export class F1SimulatorApp {
|
|
|
903
964
|
renderTiming(cars, raceMode) {
|
|
904
965
|
if (!this.timingList) return;
|
|
905
966
|
this.syncTimingGapModeControls();
|
|
906
|
-
|
|
967
|
+
const timingMarkup = cars.map((car) => {
|
|
907
968
|
const driver = this.driverById.get(car.id);
|
|
908
969
|
let gap = 'Leader';
|
|
909
970
|
if (raceMode === 'finished') {
|
|
@@ -935,6 +996,10 @@ export class F1SimulatorApp {
|
|
|
935
996
|
</li>
|
|
936
997
|
`;
|
|
937
998
|
}).join('');
|
|
999
|
+
if (timingMarkup !== this.lastTimingMarkup) {
|
|
1000
|
+
this.timingList.innerHTML = timingMarkup;
|
|
1001
|
+
this.lastTimingMarkup = timingMarkup;
|
|
1002
|
+
}
|
|
938
1003
|
}
|
|
939
1004
|
|
|
940
1005
|
formatTimingGap(car) {
|
|
@@ -962,22 +1027,27 @@ export class F1SimulatorApp {
|
|
|
962
1027
|
const drsState = car.drsActive ? 'OPEN' : car.drsEligible ? 'READY' : 'OFF';
|
|
963
1028
|
const surface = (car.surface ?? 'track').toUpperCase();
|
|
964
1029
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1030
|
+
setTextAll(this.readouts.selectedCode, car.code);
|
|
1031
|
+
this.readouts.selectedCode?.forEach?.((node) => {
|
|
1032
|
+
node.style.color = car.color;
|
|
1033
|
+
});
|
|
1034
|
+
this.readouts.telemetrySectorBanners?.forEach?.((node) => {
|
|
1035
|
+
node.style.setProperty('--driver-color', car.color);
|
|
1036
|
+
});
|
|
1037
|
+
setTextAll(this.readouts.selectedName, car.name);
|
|
1038
|
+
setTextAll(this.readouts.speed, `${Math.round(car.speedKph)} km/h`);
|
|
1039
|
+
setTextAll(this.readouts.throttle, `${Math.round(car.throttle * 100)}%`);
|
|
1040
|
+
setTextAll(this.readouts.brake, `${Math.round(car.brake * 100)}%`);
|
|
1041
|
+
setTextAll(this.readouts.tyres, `${Math.round(car.tireEnergy ?? 100)}%`);
|
|
1042
|
+
setTextAll(this.readouts.selectedDrs, drsState);
|
|
1043
|
+
setTextAll(this.readouts.surface, surface);
|
|
1044
|
+
setTextAll(
|
|
975
1045
|
this.readouts.gap,
|
|
976
1046
|
car.rank === 1 || !Number.isFinite(car.gapAheadSeconds)
|
|
977
1047
|
? '--'
|
|
978
1048
|
: `${car.gapAheadSeconds.toFixed(2)}s`,
|
|
979
1049
|
);
|
|
980
|
-
|
|
1050
|
+
setTextAll(
|
|
981
1051
|
this.readouts.leaderGap,
|
|
982
1052
|
car.rank === 1 || !Number.isFinite(car.leaderGapSeconds)
|
|
983
1053
|
? '--'
|
|
@@ -985,6 +1055,56 @@ export class F1SimulatorApp {
|
|
|
985
1055
|
);
|
|
986
1056
|
|
|
987
1057
|
this.renderCarDriverOverview(car);
|
|
1058
|
+
this.renderLapTelemetry(car.lapTelemetry);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
renderLapTelemetry(telemetry) {
|
|
1062
|
+
if (!telemetry) return;
|
|
1063
|
+
|
|
1064
|
+
setTextAll(this.readouts.currentSector, `S${telemetry.currentSector ?? 1}`);
|
|
1065
|
+
setTextAll(this.readouts.completedLaps, `${telemetry.completedLaps ?? 0} laps`);
|
|
1066
|
+
setTextAll(this.readouts.currentLapTime, formatTelemetryTime(telemetry.currentLapTime));
|
|
1067
|
+
setTextAll(this.readouts.lastLapTime, formatTelemetryTime(telemetry.lastLapTime));
|
|
1068
|
+
setTextAll(this.readouts.bestLapTime, formatTelemetryTime(telemetry.bestLapTime));
|
|
1069
|
+
|
|
1070
|
+
this.readouts.telemetrySectorBars?.forEach((bar) => {
|
|
1071
|
+
const sector = Number(bar.dataset.telemetrySectorBar);
|
|
1072
|
+
const index = sector - 1;
|
|
1073
|
+
const isActive = sector === telemetry.currentSector;
|
|
1074
|
+
const sectorComplete = Number.isFinite(telemetry.currentSectors?.[index]);
|
|
1075
|
+
const fill = sectorComplete
|
|
1076
|
+
? 100
|
|
1077
|
+
: (isActive ? clamp((telemetry.currentSectorProgress ?? 0) * 100, 0, 100) : 0);
|
|
1078
|
+
const fillValue = `${fill.toFixed(1)}%`;
|
|
1079
|
+
if (bar.style.getPropertyValue('--sector-fill') !== fillValue) {
|
|
1080
|
+
bar.style.setProperty('--sector-fill', fillValue);
|
|
1081
|
+
}
|
|
1082
|
+
bar.classList.toggle('is-active', isActive);
|
|
1083
|
+
bar.classList.toggle('is-complete', sectorComplete);
|
|
1084
|
+
setPerformanceClass(bar, telemetry.sectorPerformance?.current?.[index]);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
this.readouts.telemetrySectorTimes?.forEach((node) => {
|
|
1088
|
+
const sector = Number(node.dataset.telemetrySectorTime);
|
|
1089
|
+
const index = sector - 1;
|
|
1090
|
+
const value = sector === telemetry.currentSector && !Number.isFinite(telemetry.currentSectors?.[index])
|
|
1091
|
+
? telemetry.currentSectorElapsed
|
|
1092
|
+
: telemetry.currentSectors?.[index];
|
|
1093
|
+
setText(node, formatTelemetryTime(value));
|
|
1094
|
+
setPerformanceClass(node, telemetry.sectorPerformance?.current?.[index]);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
this.readouts.telemetrySectorLast?.forEach((node) => {
|
|
1098
|
+
const index = Number(node.dataset.telemetrySectorLast) - 1;
|
|
1099
|
+
setText(node, formatTelemetryTime(telemetry.lastSectors?.[index]));
|
|
1100
|
+
setPerformanceClass(node, telemetry.sectorPerformance?.last?.[index]);
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
this.readouts.telemetrySectorBest?.forEach((node) => {
|
|
1104
|
+
const index = Number(node.dataset.telemetrySectorBest) - 1;
|
|
1105
|
+
setText(node, formatTelemetryTime(telemetry.bestSectors?.[index]));
|
|
1106
|
+
setPerformanceClass(node, telemetry.sectorPerformance?.best?.[index]);
|
|
1107
|
+
});
|
|
988
1108
|
}
|
|
989
1109
|
|
|
990
1110
|
getOverviewFields(driver, mode) {
|
|
@@ -1024,6 +1144,25 @@ export class F1SimulatorApp {
|
|
|
1024
1144
|
const displayFields = fields.length > 0
|
|
1025
1145
|
? fields
|
|
1026
1146
|
: [{ label: mode === 'driver' ? 'Driver fields' : 'Car fields', value: 'No custom fields' }];
|
|
1147
|
+
const imageSrc = mode === 'driver'
|
|
1148
|
+
? (driver?.driverImage ?? driver?.portrait ?? driver?.avatar ?? this.assets.driverHelmet)
|
|
1149
|
+
: this.assets.carOverview;
|
|
1150
|
+
const overviewCode = mode === 'driver'
|
|
1151
|
+
? `${car.code} driver`
|
|
1152
|
+
: `${driver?.vehicle?.name ?? car.vehicleName ?? car.code}`;
|
|
1153
|
+
const overviewKey = JSON.stringify({
|
|
1154
|
+
id: car.id,
|
|
1155
|
+
mode,
|
|
1156
|
+
color: car.color,
|
|
1157
|
+
code: car.code,
|
|
1158
|
+
icon,
|
|
1159
|
+
driverNumber,
|
|
1160
|
+
imageSrc,
|
|
1161
|
+
overviewCode,
|
|
1162
|
+
fields: displayFields.map((field) => [field.label, field.value]),
|
|
1163
|
+
});
|
|
1164
|
+
if (overviewKey === this.lastOverviewRenderKey) return;
|
|
1165
|
+
this.lastOverviewRenderKey = overviewKey;
|
|
1027
1166
|
|
|
1028
1167
|
this.readouts.carOverview?.style.setProperty('--driver-color', car.color);
|
|
1029
1168
|
this.readouts.carOverviewDiagram?.style.setProperty('--driver-color', car.color);
|
|
@@ -1031,15 +1170,11 @@ export class F1SimulatorApp {
|
|
|
1031
1170
|
this.readouts.carOverviewTitle.textContent = mode === 'driver' ? 'Driver overview' : 'Car overview';
|
|
1032
1171
|
}
|
|
1033
1172
|
if (this.readouts.carOverviewCode) {
|
|
1034
|
-
this.readouts.carOverviewCode.textContent =
|
|
1035
|
-
? `${car.code} driver`
|
|
1036
|
-
: `${driver?.vehicle?.name ?? car.vehicleName ?? car.code}`;
|
|
1173
|
+
this.readouts.carOverviewCode.textContent = overviewCode;
|
|
1037
1174
|
}
|
|
1038
1175
|
if (this.readouts.carOverviewIcon) this.readouts.carOverviewIcon.textContent = icon;
|
|
1039
1176
|
if (this.readouts.carOverviewImage) {
|
|
1040
|
-
this.readouts.carOverviewImage.src =
|
|
1041
|
-
? (driver?.driverImage ?? driver?.portrait ?? driver?.avatar ?? this.assets.driverHelmet)
|
|
1042
|
-
: this.assets.carOverview;
|
|
1177
|
+
this.readouts.carOverviewImage.src = imageSrc;
|
|
1043
1178
|
}
|
|
1044
1179
|
if (this.readouts.carOverviewNumber) this.readouts.carOverviewNumber.textContent = driverNumber;
|
|
1045
1180
|
if (this.readouts.carOverviewCoreStat) this.readouts.carOverviewCoreStat.textContent = mode === 'driver' ? 'Driver' : 'Car';
|
|
@@ -1255,6 +1390,9 @@ export class F1SimulatorApp {
|
|
|
1255
1390
|
}
|
|
1256
1391
|
this.sim = this.createRaceSimulation();
|
|
1257
1392
|
this.selectedId = this.drivers[0]?.id ?? null;
|
|
1393
|
+
this.lastTimingMarkup = null;
|
|
1394
|
+
this.lastOverviewRenderKey = null;
|
|
1395
|
+
this.lastFinishClassificationMarkup = null;
|
|
1258
1396
|
this.resetRaceDataBannerState(performance.now());
|
|
1259
1397
|
this.lastTimingRenderTime = 0;
|
|
1260
1398
|
this.lastTimingRaceMode = null;
|
package/src/app/domBindings.js
CHANGED
|
@@ -13,16 +13,26 @@ export function querySimulatorDom(root) {
|
|
|
13
13
|
contacts: root.querySelector('[data-contact-readout]'),
|
|
14
14
|
camera: root.querySelector('[data-camera-readout]'),
|
|
15
15
|
fps: root.querySelector('[data-fps-readout]'),
|
|
16
|
-
selectedCode: root.
|
|
17
|
-
selectedName: root.
|
|
18
|
-
speed: root.
|
|
19
|
-
throttle: root.
|
|
20
|
-
brake: root.
|
|
21
|
-
tyres: root.
|
|
22
|
-
selectedDrs: root.
|
|
23
|
-
surface: root.
|
|
24
|
-
gap: root.
|
|
25
|
-
leaderGap: root.
|
|
16
|
+
selectedCode: root.querySelectorAll('[data-selected-code]'),
|
|
17
|
+
selectedName: root.querySelectorAll('[data-selected-name]'),
|
|
18
|
+
speed: root.querySelectorAll('[data-telemetry-speed]'),
|
|
19
|
+
throttle: root.querySelectorAll('[data-telemetry-throttle]'),
|
|
20
|
+
brake: root.querySelectorAll('[data-telemetry-brake]'),
|
|
21
|
+
tyres: root.querySelectorAll('[data-telemetry-tyres]'),
|
|
22
|
+
selectedDrs: root.querySelectorAll('[data-telemetry-drs]'),
|
|
23
|
+
surface: root.querySelectorAll('[data-telemetry-surface]'),
|
|
24
|
+
gap: root.querySelectorAll('[data-telemetry-gap]'),
|
|
25
|
+
leaderGap: root.querySelectorAll('[data-telemetry-leader-gap]'),
|
|
26
|
+
currentSector: root.querySelectorAll('[data-telemetry-current-sector]'),
|
|
27
|
+
completedLaps: root.querySelectorAll('[data-telemetry-completed-laps]'),
|
|
28
|
+
currentLapTime: root.querySelectorAll('[data-telemetry-current-lap-time]'),
|
|
29
|
+
lastLapTime: root.querySelectorAll('[data-telemetry-last-lap-time]'),
|
|
30
|
+
bestLapTime: root.querySelectorAll('[data-telemetry-best-lap-time]'),
|
|
31
|
+
telemetrySectorBars: root.querySelectorAll('[data-telemetry-sector-bar]'),
|
|
32
|
+
telemetrySectorTimes: root.querySelectorAll('[data-telemetry-sector-time]'),
|
|
33
|
+
telemetrySectorLast: root.querySelectorAll('[data-telemetry-sector-last]'),
|
|
34
|
+
telemetrySectorBest: root.querySelectorAll('[data-telemetry-sector-best]'),
|
|
35
|
+
telemetrySectorBanners: root.querySelectorAll('[data-telemetry-sector-banner]'),
|
|
26
36
|
carOverview: root.querySelector('[data-paddock-component="car-driver-overview"]'),
|
|
27
37
|
carOverviewTitle: root.querySelector('[data-car-overview-title]'),
|
|
28
38
|
carOverviewDiagram: root.querySelector('.car-overview-diagram'),
|
|
@@ -32,6 +42,10 @@ export function querySimulatorDom(root) {
|
|
|
32
42
|
carOverviewNumber: root.querySelector('[data-car-overview-number]'),
|
|
33
43
|
carOverviewCoreStat: root.querySelector('[data-car-overview-core-stat]'),
|
|
34
44
|
carOverviewFields: root.querySelectorAll('[data-overview-field]'),
|
|
45
|
+
telemetryDrawerWorkbench: root.querySelector('[data-race-telemetry-drawer]'),
|
|
46
|
+
telemetryDrawer: root.querySelector('[data-telemetry-drawer]'),
|
|
47
|
+
telemetryDrawerToggle: root.querySelector('[data-telemetry-drawer-toggle]'),
|
|
48
|
+
telemetryDrawerClose: root.querySelector('[data-telemetry-drawer-close]'),
|
|
35
49
|
raceDataPanel: root.querySelector('[data-race-data-panel]'),
|
|
36
50
|
raceDataKicker: root.querySelector('[data-race-data-kicker]'),
|
|
37
51
|
raceDataTitle: root.querySelector('[data-race-data-title]'),
|
|
@@ -60,5 +74,16 @@ export function querySimulatorDom(root) {
|
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
export function setText(node, value) {
|
|
63
|
-
if (node)
|
|
77
|
+
if (!node) return;
|
|
78
|
+
const nextValue = String(value ?? '');
|
|
79
|
+
if (node.textContent !== nextValue) node.textContent = nextValue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function setTextAll(nodes, value) {
|
|
83
|
+
if (!nodes) return;
|
|
84
|
+
if (typeof nodes.forEach === 'function') {
|
|
85
|
+
nodes.forEach((node) => setText(node, value));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setText(nodes, value);
|
|
64
89
|
}
|
|
@@ -18,6 +18,12 @@ export const DEFAULT_F1_SIMULATOR_OPTIONS = {
|
|
|
18
18
|
showTimingTower: true,
|
|
19
19
|
showTelemetry: true,
|
|
20
20
|
telemetryIncludesOverview: true,
|
|
21
|
+
telemetryModules: {
|
|
22
|
+
core: true,
|
|
23
|
+
sectors: true,
|
|
24
|
+
lapTimes: true,
|
|
25
|
+
sectorTimes: true,
|
|
26
|
+
},
|
|
21
27
|
showRaceDataPanel: true,
|
|
22
28
|
raceDataBanners: {
|
|
23
29
|
initial: 'project',
|
|
@@ -121,6 +127,7 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
121
127
|
},
|
|
122
128
|
};
|
|
123
129
|
ui.raceDataBanners.enabled = normalizeEnabledBanners(ui.raceDataBanners.enabled);
|
|
130
|
+
ui.telemetryModules = normalizeTelemetryModules(ui.telemetryModules);
|
|
124
131
|
if (!['project', 'radio', 'hidden'].includes(ui.raceDataBanners.initial)) {
|
|
125
132
|
ui.raceDataBanners.initial = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.initial;
|
|
126
133
|
}
|
|
@@ -174,6 +181,24 @@ function normalizeEnabledBanners(value) {
|
|
|
174
181
|
return [...new Set(value.filter((item) => item === 'project' || item === 'radio'))];
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
function normalizeTelemetryModules(value) {
|
|
185
|
+
const defaults = DEFAULT_F1_SIMULATOR_OPTIONS.ui.telemetryModules;
|
|
186
|
+
const names = Object.keys(defaults);
|
|
187
|
+
if (value === false) {
|
|
188
|
+
return Object.fromEntries(names.map((name) => [name, false]));
|
|
189
|
+
}
|
|
190
|
+
if (value === true || value == null) return { ...defaults };
|
|
191
|
+
if (Array.isArray(value)) {
|
|
192
|
+
const requested = new Set(value);
|
|
193
|
+
return Object.fromEntries(names.map((name) => [name, requested.has(name)]));
|
|
194
|
+
}
|
|
195
|
+
if (typeof value !== 'object') return { ...defaults };
|
|
196
|
+
return Object.fromEntries(names.map((name) => [
|
|
197
|
+
name,
|
|
198
|
+
value[name] == null ? defaults[name] : Boolean(value[name]),
|
|
199
|
+
]));
|
|
200
|
+
}
|
|
201
|
+
|
|
177
202
|
function normalizeTheme(theme) {
|
|
178
203
|
return Object.fromEntries(
|
|
179
204
|
Object.entries(DEFAULT_F1_SIMULATOR_OPTIONS.theme).map(([key, fallback]) => [
|
package/src/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type TimingTowerVerticalFit = 'expand-race-view' | 'scroll';
|
|
|
7
7
|
export type LayoutPreset = 'standard' | 'left-tower-overlay';
|
|
8
8
|
export type CameraControlsMode = 'embedded' | 'external' | false;
|
|
9
9
|
export type PaddockPresetName = 'dashboard' | 'timing-overlay' | 'compact-race' | 'full-dashboard';
|
|
10
|
+
export type TelemetryModuleName = 'core' | 'sectors' | 'lapTimes' | 'sectorTimes';
|
|
11
|
+
export type SectorPerformanceStatus = 'overall-best' | 'personal-best' | 'slower';
|
|
10
12
|
|
|
11
13
|
export interface CustomField {
|
|
12
14
|
label: string;
|
|
@@ -170,6 +172,7 @@ export interface F1SimulatorUiOptions {
|
|
|
170
172
|
showTimingTower?: boolean;
|
|
171
173
|
showTelemetry?: boolean;
|
|
172
174
|
telemetryIncludesOverview?: boolean;
|
|
175
|
+
telemetryModules?: boolean | TelemetryModuleName[] | Partial<Record<TelemetryModuleName, boolean>>;
|
|
173
176
|
showRaceDataPanel?: boolean;
|
|
174
177
|
raceDataBanners?: {
|
|
175
178
|
initial?: RaceBannerMode;
|
|
@@ -197,9 +200,40 @@ export interface TrackSnapshot {
|
|
|
197
200
|
seed?: number | null;
|
|
198
201
|
width?: number;
|
|
199
202
|
length?: number;
|
|
203
|
+
sectors?: TrackSectorSnapshot[];
|
|
200
204
|
[key: string]: unknown;
|
|
201
205
|
}
|
|
202
206
|
|
|
207
|
+
export interface TrackSectorSnapshot {
|
|
208
|
+
index: number;
|
|
209
|
+
id: string;
|
|
210
|
+
label: string;
|
|
211
|
+
start: number;
|
|
212
|
+
end: number;
|
|
213
|
+
startRatio: number;
|
|
214
|
+
endRatio: number;
|
|
215
|
+
length: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface LapTelemetrySnapshot {
|
|
219
|
+
currentLap: number;
|
|
220
|
+
currentSector: number;
|
|
221
|
+
currentLapTime: number | null;
|
|
222
|
+
currentSectorElapsed: number | null;
|
|
223
|
+
currentSectorProgress: number | null;
|
|
224
|
+
currentSectors: Array<number | null>;
|
|
225
|
+
sectorPerformance: {
|
|
226
|
+
current: Array<SectorPerformanceStatus | null>;
|
|
227
|
+
last: Array<SectorPerformanceStatus | null>;
|
|
228
|
+
best: Array<SectorPerformanceStatus | null>;
|
|
229
|
+
};
|
|
230
|
+
lastLapTime: number | null;
|
|
231
|
+
bestLapTime: number | null;
|
|
232
|
+
lastSectors: Array<number | null>;
|
|
233
|
+
bestSectors: Array<number | null>;
|
|
234
|
+
completedLaps: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
203
237
|
export interface RaceEvent {
|
|
204
238
|
type: string;
|
|
205
239
|
at?: number;
|
|
@@ -221,6 +255,7 @@ export interface CarSnapshot {
|
|
|
221
255
|
classifiedRank?: number | null;
|
|
222
256
|
intervalAheadSeconds?: number | null;
|
|
223
257
|
leaderGapSeconds?: number | null;
|
|
258
|
+
lapTelemetry?: LapTelemetrySnapshot;
|
|
224
259
|
[key: string]: unknown;
|
|
225
260
|
}
|
|
226
261
|
|
|
@@ -309,6 +344,7 @@ export interface F1SimulatorOptions extends F1SimulatorCallbacks {
|
|
|
309
344
|
export interface MountRaceCanvasOptions {
|
|
310
345
|
includeRaceDataPanel?: boolean;
|
|
311
346
|
includeTimingTower?: boolean;
|
|
347
|
+
includeTelemetrySectorBanner?: boolean;
|
|
312
348
|
timingTowerVerticalFit?: TimingTowerVerticalFit;
|
|
313
349
|
}
|
|
314
350
|
|
|
@@ -316,6 +352,11 @@ export interface MountTelemetryPanelOptions {
|
|
|
316
352
|
includeOverview?: boolean;
|
|
317
353
|
}
|
|
318
354
|
|
|
355
|
+
export interface MountRaceTelemetryDrawerOptions {
|
|
356
|
+
timingTowerVerticalFit?: TimingTowerVerticalFit;
|
|
357
|
+
drawerInitiallyOpen?: boolean;
|
|
358
|
+
}
|
|
359
|
+
|
|
319
360
|
export interface F1MountedSimulator {
|
|
320
361
|
destroy(): void;
|
|
321
362
|
restart(nextOptions?: Partial<F1SimulatorOptions>): void;
|
|
@@ -334,6 +375,12 @@ export interface PaddockSimulatorController {
|
|
|
334
375
|
mountTimingTower<T extends Element>(root: T): T;
|
|
335
376
|
mountRaceCanvas<T extends Element>(root: T, options?: MountRaceCanvasOptions): T;
|
|
336
377
|
mountTelemetryPanel<T extends Element>(root: T, options?: MountTelemetryPanelOptions): T;
|
|
378
|
+
mountTelemetryCore<T extends Element>(root: T): T;
|
|
379
|
+
mountTelemetrySectors<T extends Element>(root: T): T;
|
|
380
|
+
mountTelemetrySectorBanner<T extends Element>(root: T): T;
|
|
381
|
+
mountTelemetryLapTimes<T extends Element>(root: T): T;
|
|
382
|
+
mountTelemetrySectorTimes<T extends Element>(root: T): T;
|
|
383
|
+
mountRaceTelemetryDrawer<T extends Element>(root: T, options?: MountRaceTelemetryDrawerOptions): T;
|
|
337
384
|
mountCarDriverOverview<T extends Element>(root: T): T;
|
|
338
385
|
mountRaceDataPanel<T extends Element>(root: T): T;
|
|
339
386
|
querySelector(selector: string): Element | null;
|
|
@@ -395,5 +442,15 @@ export function mountTelemetryPanel<T extends Element>(
|
|
|
395
442
|
simulator: PaddockSimulatorController,
|
|
396
443
|
options?: MountTelemetryPanelOptions,
|
|
397
444
|
): T;
|
|
445
|
+
export function mountTelemetryCore<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
446
|
+
export function mountTelemetrySectors<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
447
|
+
export function mountTelemetrySectorBanner<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
448
|
+
export function mountTelemetryLapTimes<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
449
|
+
export function mountTelemetrySectorTimes<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
450
|
+
export function mountRaceTelemetryDrawer<T extends Element>(
|
|
451
|
+
root: T,
|
|
452
|
+
simulator: PaddockSimulatorController,
|
|
453
|
+
options?: MountRaceTelemetryDrawerOptions,
|
|
454
|
+
): T;
|
|
398
455
|
export function mountCarDriverOverview<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
|
399
456
|
export function mountRaceDataPanel<T extends Element>(root: T, simulator: PaddockSimulatorController): T;
|
package/src/index.js
CHANGED
|
@@ -9,8 +9,14 @@ export {
|
|
|
9
9
|
mountRaceCanvas,
|
|
10
10
|
mountRaceControls,
|
|
11
11
|
mountRaceDataPanel,
|
|
12
|
+
mountRaceTelemetryDrawer,
|
|
12
13
|
mountSafetyCarControl,
|
|
14
|
+
mountTelemetryCore,
|
|
15
|
+
mountTelemetryLapTimes,
|
|
13
16
|
mountTelemetryPanel,
|
|
17
|
+
mountTelemetrySectorBanner,
|
|
18
|
+
mountTelemetrySectorTimes,
|
|
19
|
+
mountTelemetrySectors,
|
|
14
20
|
mountTimingTower,
|
|
15
21
|
} from './api/PaddockSimulatorController.js';
|
|
16
22
|
|