@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.
@@ -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 overlapsCanvas = towerRect.right > canvasRect.left && towerRect.left < canvasRect.right;
641
- if (!overlapsCanvas) {
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
- this.readouts.finishClassification.innerHTML = topThree.map((entry) => `
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
- this.timingList.innerHTML = cars.map((car) => {
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
- setText(this.readouts.selectedCode, car.code);
966
- if (this.readouts.selectedCode) this.readouts.selectedCode.style.color = car.color;
967
- setText(this.readouts.selectedName, car.name);
968
- setText(this.readouts.speed, `${Math.round(car.speedKph)} km/h`);
969
- setText(this.readouts.throttle, `${Math.round(car.throttle * 100)}%`);
970
- setText(this.readouts.brake, `${Math.round(car.brake * 100)}%`);
971
- setText(this.readouts.tyres, `${Math.round(car.tireEnergy ?? 100)}%`);
972
- setText(this.readouts.selectedDrs, drsState);
973
- setText(this.readouts.surface, surface);
974
- setText(
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
- setText(
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 = mode === 'driver'
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 = mode === 'driver'
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;
@@ -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.querySelector('[data-selected-code]'),
17
- selectedName: root.querySelector('[data-selected-name]'),
18
- speed: root.querySelector('[data-telemetry-speed]'),
19
- throttle: root.querySelector('[data-telemetry-throttle]'),
20
- brake: root.querySelector('[data-telemetry-brake]'),
21
- tyres: root.querySelector('[data-telemetry-tyres]'),
22
- selectedDrs: root.querySelector('[data-telemetry-drs]'),
23
- surface: root.querySelector('[data-telemetry-surface]'),
24
- gap: root.querySelector('[data-telemetry-gap]'),
25
- leaderGap: root.querySelector('[data-telemetry-leader-gap]'),
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) node.textContent = value;
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