@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.
Files changed (136) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +84 -512
  4. package/docs/architecture.md +38 -24
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/composable-layouts.md +77 -0
  7. package/docs/concepts.md +2 -2
  8. package/docs/custom_model_controller.md +6 -6
  9. package/docs/data-helpers.md +50 -0
  10. package/docs/data_contract.md +104 -27
  11. package/docs/getting-started.md +75 -0
  12. package/docs/headless-environment.md +68 -0
  13. package/docs/index.md +18 -4
  14. package/docs/learnings.md +6 -6
  15. package/docs/rules.md +12 -8
  16. package/docs/sense_contract.md +1 -1
  17. package/docs/system_specs.md +71 -21
  18. package/docs/theming.md +64 -0
  19. package/docs/training.md +26 -19
  20. package/docs/troubleshooting.md +76 -0
  21. package/docs/upgrading-hosts-to-3.md +42 -0
  22. package/examples/python/README.md +6 -0
  23. package/examples/python/base_policy_server.py +19 -0
  24. package/package.json +10 -3
  25. package/src/api/PaddockSimulatorController.js +150 -24
  26. package/src/api/runtimeTheme.js +59 -0
  27. package/src/app/F1SimulatorApp.js +213 -29
  28. package/src/app/banners/raceDataBannerController.js +71 -36
  29. package/src/app/banners/stewardMessageController.js +5 -4
  30. package/src/app/camera/cameraConstants.js +2 -0
  31. package/src/app/camera/cameraController.js +86 -29
  32. package/src/app/camera/cameraGeometry.js +12 -1
  33. package/src/app/domBindings.js +89 -9
  34. package/src/app/layoutScheduler.js +27 -0
  35. package/src/app/layoutSupport.js +165 -0
  36. package/src/app/raceOverlayClearanceSupport.js +158 -0
  37. package/src/app/readouts/carOverviewRenderer.js +16 -8
  38. package/src/app/readouts/controlStateRenderer.js +1 -1
  39. package/src/app/readouts/raceStatusRenderer.js +30 -21
  40. package/src/app/readouts/readoutFormatters.js +2 -0
  41. package/src/app/readouts/telemetryRenderer.js +17 -8
  42. package/src/app/readouts/timingTowerRenderer.js +262 -49
  43. package/src/app/runtime/frameLoop.js +5 -1
  44. package/src/app/runtime/runtimeVisibility.js +66 -5
  45. package/src/config/cssValues.js +16 -0
  46. package/src/config/defaultOptions.js +58 -38
  47. package/src/config/themeOptions.js +830 -0
  48. package/src/config/timingGapMode.js +13 -0
  49. package/src/config/urlOptions.js +25 -0
  50. package/src/data/championship.js +14 -9
  51. package/src/data/index.d.ts +212 -0
  52. package/src/data/index.js +14 -0
  53. package/src/environment/controllerLoop.js +17 -3
  54. package/src/environment/episode.js +11 -1
  55. package/src/environment/index.d.ts +1 -2
  56. package/src/environment/metrics.js +59 -16
  57. package/src/environment/observationVector.js +50 -33
  58. package/src/environment/observations.js +89 -20
  59. package/src/environment/options.js +1 -1
  60. package/src/environment/runtime.js +44 -11
  61. package/src/environment/scenarios.js +1 -1
  62. package/src/environment/sensors/carRays.js +48 -37
  63. package/src/environment/sensors/index.js +501 -97
  64. package/src/environment/sensors/indexedRayBands.js +134 -46
  65. package/src/environment/sensors/rayBandTrace.js +1171 -0
  66. package/src/environment/sensors/rayChannels.js +108 -0
  67. package/src/environment/sensors/rayConfig.js +71 -25
  68. package/src/environment/sensors/rayGeometry.js +23 -17
  69. package/src/environment/sensors/sensorTargets.js +67 -1
  70. package/src/environment/sensors/surfaceRays.js +167 -118
  71. package/src/environment/sensors/trackRays.js +157 -48
  72. package/src/index.d.ts +153 -9
  73. package/src/index.js +52 -3
  74. package/src/simulation/collisionGeometry.js +260 -96
  75. package/src/simulation/driver/driverController.js +1 -1
  76. package/src/simulation/driver/edgeRecovery.js +1 -1
  77. package/src/simulation/driver/racingControls.js +2 -2
  78. package/src/simulation/driver/rejoinControls.js +2 -2
  79. package/src/simulation/pit/pitFlow.js +21 -9
  80. package/src/simulation/pit/pitKinematics.js +2 -2
  81. package/src/simulation/pit/pitQueue.js +5 -3
  82. package/src/simulation/pit/pitRouteMovement.js +9 -4
  83. package/src/simulation/pit/pitRouting.js +79 -46
  84. package/src/simulation/pit/pitState.js +1 -0
  85. package/src/simulation/pit/pitTireService.js +7 -3
  86. package/src/simulation/race/raceFinish.js +25 -8
  87. package/src/simulation/race/raceOrder.js +189 -32
  88. package/src/simulation/race/raceProgress.js +129 -37
  89. package/src/simulation/race/raceSetup.js +4 -6
  90. package/src/simulation/race/raceStep.js +26 -14
  91. package/src/simulation/race/safetyCar.js +1 -1
  92. package/src/simulation/race/stalledDnf.js +1 -1
  93. package/src/simulation/raceSimulation.js +20 -17
  94. package/src/simulation/rules/collisionSteward.js +40 -23
  95. package/src/simulation/rules/rulesReview.js +77 -56
  96. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  97. package/src/simulation/timing/drsTiming.js +143 -30
  98. package/src/simulation/timing/gapEstimation.js +25 -3
  99. package/src/simulation/timing/lapTelemetry.js +138 -40
  100. package/src/simulation/timing/raceTiming.js +5 -1
  101. package/src/simulation/timing/sectorPerformance.js +104 -26
  102. package/src/simulation/timing/timingHistory.js +229 -14
  103. package/src/simulation/timing/timingLines.js +65 -7
  104. package/src/simulation/track/pitLaneLayout.js +30 -3
  105. package/src/simulation/track/pitLaneState.js +218 -19
  106. package/src/simulation/track/spatialQueries.js +66 -33
  107. package/src/simulation/track/trackMath.js +91 -24
  108. package/src/simulation/track/trackModel.js +2 -0
  109. package/src/simulation/track/trackQueryIndex.js +1468 -189
  110. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  111. package/src/simulation/track/trackQueryScratch.js +7 -0
  112. package/src/simulation/track/trackQueryStats.js +25 -0
  113. package/src/simulation/track/trackSnapshotJson.js +139 -0
  114. package/src/simulation/track/trackStatePolicy.js +249 -2
  115. package/src/simulation/vehicle/contactResolution.js +99 -62
  116. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  117. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  118. package/src/simulation/vehicle/runoffResponse.js +54 -29
  119. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  120. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  121. package/src/simulation/vehicle/vehiclePhysics.js +45 -30
  122. package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
  123. package/src/simulation/vehicle/vehicleState.js +7 -1
  124. package/src/simulation/vehicle/wheelSurface.js +807 -92
  125. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  126. package/src/styles.css +1568 -519
  127. package/src/ui/bannerTemplates.js +38 -28
  128. package/src/ui/cameraControlsTemplate.js +16 -5
  129. package/src/ui/carOverviewTemplate.js +12 -5
  130. package/src/ui/raceCanvasTemplate.js +31 -8
  131. package/src/ui/raceControlsTemplate.js +27 -10
  132. package/src/ui/shellTemplate.js +3 -1
  133. package/src/ui/telemetryDrawerTemplate.js +23 -10
  134. package/src/ui/telemetryTemplates.js +58 -32
  135. package/src/ui/templateUtils.js +48 -0
  136. package/src/ui/timingTowerTemplate.js +29 -12
@@ -0,0 +1,158 @@
1
+ const CLEARANCE_CLASS = 'sim-canvas-panel--needs-banner-clearance';
2
+ import { createRafScheduler } from './layoutScheduler.js';
3
+
4
+ const CLEARANCE_VAR = '--race-overlay-banner-clearance';
5
+ const OVERLAY_GAP_PX = 12;
6
+
7
+ function parsePixelValue(value, fallback = 0) {
8
+ const parsed = Number.parseFloat(value);
9
+ return Number.isFinite(parsed) ? parsed : fallback;
10
+ }
11
+
12
+ function isVisible(element) {
13
+ if (!element || element.hidden || element.classList?.contains('is-hidden')) return false;
14
+ const rect = element.getBoundingClientRect();
15
+ const style = getComputedStyle(element);
16
+ return rect.width > 0 &&
17
+ rect.height > 0 &&
18
+ style.display !== 'none' &&
19
+ style.visibility !== 'hidden';
20
+ }
21
+
22
+ function relativeBounds(panel, element) {
23
+ const panelRect = panel.getBoundingClientRect();
24
+ const rect = element.getBoundingClientRect();
25
+ return {
26
+ left: rect.left - panelRect.left,
27
+ right: rect.right - panelRect.left,
28
+ top: rect.top - panelRect.top,
29
+ bottom: rect.bottom - panelRect.top,
30
+ width: rect.width,
31
+ height: rect.height,
32
+ };
33
+ }
34
+
35
+ function timingEntryBounds(panel, tower) {
36
+ const panelRect = panel.getBoundingClientRect();
37
+ const towerStyle = getComputedStyle(tower);
38
+ const towerRect = tower.getBoundingClientRect();
39
+ const towerOpenLeft = parsePixelValue(towerStyle.left, towerRect.left - panelRect.left);
40
+
41
+ return Array.from(tower.querySelectorAll?.('.timing-row') ?? [])
42
+ .filter(isVisible)
43
+ .map((row) => {
44
+ const rowRect = row.getBoundingClientRect();
45
+ const rowLeftInTower = rowRect.left - towerRect.left;
46
+ const left = towerOpenLeft + rowLeftInTower;
47
+ return {
48
+ left,
49
+ right: left + rowRect.width,
50
+ top: rowRect.top - panelRect.top,
51
+ bottom: rowRect.bottom - panelRect.top,
52
+ };
53
+ });
54
+ }
55
+
56
+ function rectanglesOverlap(first, second) {
57
+ return first.left < second.right &&
58
+ first.right > second.left &&
59
+ first.top < second.bottom &&
60
+ first.bottom > second.top;
61
+ }
62
+
63
+ function createPanelOverlayRecord(panel) {
64
+ return {
65
+ panel,
66
+ tower: panel.querySelector?.('[data-timing-tower]') ?? null,
67
+ banner: panel.querySelector?.('[data-race-data-panel]') ?? null,
68
+ toggle: panel.querySelector?.('[data-timing-panel-toggle]') ?? null,
69
+ };
70
+ }
71
+
72
+ function syncPanelOverlayClearance(record) {
73
+ const { panel, tower, banner, toggle } = record;
74
+ if (!panel.classList?.contains('sim-canvas-panel--responsive-narrow')) {
75
+ panel.classList?.remove(CLEARANCE_CLASS);
76
+ panel.style?.removeProperty(CLEARANCE_VAR);
77
+ return;
78
+ }
79
+
80
+ const toggleStyle = toggle ? getComputedStyle(toggle) : null;
81
+ const timingRevealMode = Boolean(toggle && toggleStyle?.display !== 'none' && toggleStyle?.visibility !== 'hidden');
82
+ if (!timingRevealMode || !tower || !isVisible(banner)) {
83
+ panel.classList?.remove(CLEARANCE_CLASS);
84
+ panel.style?.removeProperty(CLEARANCE_VAR);
85
+ return;
86
+ }
87
+
88
+ const currentClearance = parsePixelValue(getComputedStyle(panel).getPropertyValue(CLEARANCE_VAR), 0);
89
+ const bannerBounds = relativeBounds(panel, banner);
90
+ const bannerWithoutClearance = {
91
+ ...bannerBounds,
92
+ top: bannerBounds.top - currentClearance,
93
+ bottom: bannerBounds.bottom - currentClearance,
94
+ };
95
+ const needsClearance = timingEntryBounds(panel, tower)
96
+ .some((entryBounds) => rectanglesOverlap(entryBounds, bannerWithoutClearance));
97
+
98
+ panel.classList?.toggle(CLEARANCE_CLASS, needsClearance);
99
+ if (!needsClearance) {
100
+ panel.style?.removeProperty(CLEARANCE_VAR);
101
+ return;
102
+ }
103
+
104
+ const panelRect = panel.getBoundingClientRect();
105
+ const bannerRect = banner.getBoundingClientRect();
106
+ const clearance = Math.max(0, Math.ceil(panelRect.bottom - bannerRect.top + OVERLAY_GAP_PX));
107
+ panel.style?.setProperty(CLEARANCE_VAR, `${clearance}px`);
108
+ }
109
+
110
+ function raceCanvasPanels(root) {
111
+ const queryRoot = root?.querySelectorAll ? root : null;
112
+ if (!queryRoot) return [];
113
+ const panels = [];
114
+ if (root.matches?.('.sim-canvas-panel--with-timing-tower')) panels.push(root);
115
+ panels.push(...queryRoot.querySelectorAll('.sim-canvas-panel--with-timing-tower'));
116
+ return [...new Set(panels)];
117
+ }
118
+
119
+ export function installRaceOverlayClearanceSupport(root) {
120
+ const panelRecords = raceCanvasPanels(root).map(createPanelOverlayRecord);
121
+ if (panelRecords.length === 0) return () => {};
122
+
123
+ const syncPanels = () => panelRecords.forEach(syncPanelOverlayClearance);
124
+ const scheduler = createRafScheduler(syncPanels);
125
+
126
+ scheduler.runNow();
127
+
128
+ const resizeObserver = typeof ResizeObserver === 'function'
129
+ ? new ResizeObserver(scheduler.queue)
130
+ : null;
131
+ panelRecords.forEach(({ panel, tower, banner }) => {
132
+ resizeObserver?.observe(panel);
133
+ if (tower) resizeObserver?.observe(tower);
134
+ if (banner) resizeObserver?.observe(banner);
135
+ });
136
+
137
+ const mutationObserver = typeof MutationObserver === 'function'
138
+ ? new MutationObserver(scheduler.queue)
139
+ : null;
140
+ panelRecords.forEach(({ panel }) => {
141
+ mutationObserver?.observe(panel, {
142
+ attributes: true,
143
+ attributeFilter: ['aria-hidden', 'class', 'hidden', 'style'],
144
+ childList: true,
145
+ subtree: true,
146
+ });
147
+ });
148
+
149
+ if (typeof requestAnimationFrame === 'function') {
150
+ scheduler.queue();
151
+ }
152
+
153
+ return () => {
154
+ resizeObserver?.disconnect();
155
+ mutationObserver?.disconnect();
156
+ scheduler.cancel();
157
+ };
158
+ }
@@ -1,6 +1,7 @@
1
1
  import { DRIVER_STAT_DEFINITIONS, formatDriverNumber, VEHICLE_STAT_DEFINITIONS } from '../../data/championship.js';
2
2
  import { normalizeCustomFields } from '../../data/customFields.js';
3
- import { setText } from '../domBindings.js';
3
+ import { setStyleProperty, setText } from '../domBindings.js';
4
+ import { formatCssColor } from './readoutFormatters.js';
4
5
 
5
6
  const VEHICLE_OVERVIEW_FIELDS = [
6
7
  ['power', 'Power'],
@@ -82,8 +83,9 @@ export function renderCarDriverOverview({
82
83
  });
83
84
  if (overviewKey === lastOverviewRenderKey) return lastOverviewRenderKey;
84
85
 
85
- readouts.carOverview?.style.setProperty('--driver-color', car.color);
86
- readouts.carOverviewDiagram?.style.setProperty('--driver-color', car.color);
86
+ const driverColor = formatCssColor(car.color);
87
+ setStyleProperty(readouts.carOverview, '--driver-color', driverColor);
88
+ setStyleProperty(readouts.carOverviewDiagram, '--driver-color', driverColor);
87
89
  if (readouts.carOverviewTitle) {
88
90
  readouts.carOverviewTitle.textContent = mode === 'driver' ? 'Driver overview' : 'Car overview';
89
91
  }
@@ -96,14 +98,20 @@ export function renderCarDriverOverview({
96
98
  }
97
99
  if (readouts.carOverviewNumber) readouts.carOverviewNumber.textContent = driverNumber;
98
100
  if (readouts.carOverviewCoreStat) readouts.carOverviewCoreStat.textContent = mode === 'driver' ? 'Driver' : 'Car';
99
- readouts.carOverviewFields.forEach((fieldNode, index) => {
101
+ const fieldBindings = readouts.carOverviewFieldBindings?.length
102
+ ? readouts.carOverviewFieldBindings
103
+ : [...(readouts.carOverviewFields ?? [])].map((fieldNode) => ({
104
+ field: fieldNode,
105
+ label: fieldNode.querySelector?.('[data-overview-field-label]') ?? null,
106
+ value: fieldNode.querySelector?.('[data-overview-field-value]') ?? null,
107
+ }));
108
+ fieldBindings.forEach((fieldBinding, index) => {
100
109
  const field = displayFields[index];
110
+ const fieldNode = fieldBinding.field;
101
111
  fieldNode.hidden = !field;
102
112
  if (!field) return;
103
- const labelNode = fieldNode.querySelector('[data-overview-field-label]');
104
- const valueNode = fieldNode.querySelector('[data-overview-field-value]');
105
- setText(labelNode, field.label);
106
- setText(valueNode, field.value);
113
+ setText(fieldBinding.label, field.label);
114
+ setText(fieldBinding.value, field.value);
107
115
  });
108
116
  return overviewKey;
109
117
  }
@@ -9,7 +9,7 @@ export function updateCameraControlButtons({ camera, cameraButtons, snapshot, ha
9
9
  cameraButtons.forEach((button) => {
10
10
  const mode = button.dataset.cameraMode;
11
11
  const isAvailable = isCameraModeAvailable(mode, snapshot);
12
- if (mode === 'pit') {
12
+ if (mode === 'pit' || mode === 'driver') {
13
13
  button.hidden = !isAvailable;
14
14
  button.disabled = !isAvailable;
15
15
  button.setAttribute('aria-hidden', String(!isAvailable));
@@ -1,6 +1,12 @@
1
1
  import { getRaceControlStatusBanner } from '../../ui/raceControlStatusBanner.js';
2
- import { setText } from '../domBindings.js';
3
- import { escapeHtml } from './readoutFormatters.js';
2
+ import { resolveNodes, setStyleProperty, setText } from '../domBindings.js';
3
+ import { escapeHtml, formatCssColor } from './readoutFormatters.js';
4
+
5
+ function setTextEvery(nodes, node, value) {
6
+ resolveNodes(nodes, node).forEach((target) => {
7
+ if (target.textContent !== value) target.textContent = value;
8
+ });
9
+ }
4
10
 
5
11
  export function renderStartLights(readouts, startLightNodes, raceControl) {
6
12
  const panel = readouts.startLights;
@@ -33,7 +39,7 @@ export function renderRaceFinish({ readouts, snapshot, driverById, lastFinishCla
33
39
  const winner = snapshot.raceControl.winner;
34
40
  const winnerDriver = winner ? driverById.get(winner.id) : null;
35
41
  const winnerName = winnerDriver?.name ?? winner?.name ?? 'Winner';
36
- panel.style.setProperty('--driver-color', winner?.color ?? winnerDriver?.color ?? 'var(--red)');
42
+ setStyleProperty(panel, '--driver-color', formatCssColor(winner?.color ?? winnerDriver?.color));
37
43
  setText(readouts.finishWinner, winnerName);
38
44
 
39
45
  if (readouts.finishClassification) {
@@ -84,26 +90,29 @@ export function renderRaceStatusReadouts({
84
90
  : 'var(--green)';
85
91
  }
86
92
  if (readouts.lap) readouts.lap.textContent = `${leader?.lap ?? 1}/${snapshot.totalLaps}`;
87
- if (readouts.towerLap) readouts.towerLap.textContent = leader?.lap ?? 1;
88
- if (readouts.towerTotalLaps) readouts.towerTotalLaps.textContent = snapshot.totalLaps;
89
- if (readouts.timingTower) {
90
- readouts.timingTower.classList.toggle('is-safety-car', snapshot.raceControl.mode === 'safety-car');
91
- readouts.timingTower.classList.toggle('is-red-flag', snapshot.raceControl.mode === 'red-flag');
92
- readouts.timingTower.classList.toggle('is-pre-start', snapshot.raceControl.mode === 'pre-start');
93
- }
94
- if (readouts.towerRaceControlBanner) {
93
+ setTextEvery(readouts.towerLaps, readouts.towerLap, leader?.lap ?? 1);
94
+ setTextEvery(readouts.towerTotalLapsAll, readouts.towerTotalLaps, snapshot.totalLaps);
95
+ resolveNodes(readouts.timingTowers, readouts.timingTower).forEach((timingTower) => {
96
+ timingTower.classList.toggle('is-safety-car', snapshot.raceControl.mode === 'safety-car');
97
+ timingTower.classList.toggle('is-red-flag', snapshot.raceControl.mode === 'red-flag');
98
+ timingTower.classList.toggle('is-pre-start', snapshot.raceControl.mode === 'pre-start');
99
+ });
100
+ const raceControlBanners = resolveNodes(readouts.towerRaceControlBanners, readouts.towerRaceControlBanner);
101
+ if (raceControlBanners.length > 0) {
95
102
  const raceControlBanner = getRaceControlStatusBanner(snapshot.raceControl.mode);
96
- readouts.towerRaceControlBanner.hidden = !raceControlBanner;
97
- readouts.towerRaceControlBanner.classList?.toggle?.('is-safety-car', raceControlBanner?.status === 'safety-car');
98
- readouts.towerRaceControlBanner.classList?.toggle?.('is-red-flag', raceControlBanner?.status === 'red-flag');
99
- if (raceControlBanner && readouts.towerRaceControlBanner.dataset) {
100
- readouts.towerRaceControlBanner.dataset.raceControlStatus = raceControlBanner.status;
101
- } else if (readouts.towerRaceControlBanner.dataset) {
102
- delete readouts.towerRaceControlBanner.dataset.raceControlStatus;
103
- }
103
+ raceControlBanners.forEach((banner) => {
104
+ banner.hidden = !raceControlBanner;
105
+ banner.classList?.toggle?.('is-safety-car', raceControlBanner?.status === 'safety-car');
106
+ banner.classList?.toggle?.('is-red-flag', raceControlBanner?.status === 'red-flag');
107
+ if (raceControlBanner && banner.dataset) {
108
+ banner.dataset.raceControlStatus = raceControlBanner.status;
109
+ } else if (banner.dataset) {
110
+ delete banner.dataset.raceControlStatus;
111
+ }
112
+ });
104
113
  if (raceControlBanner) {
105
- setText(readouts.towerRaceControlKicker, raceControlBanner.kicker);
106
- setText(readouts.towerRaceControlTitle, raceControlBanner.title);
114
+ setTextEvery(readouts.towerRaceControlKickers, readouts.towerRaceControlKicker, raceControlBanner.kicker);
115
+ setTextEvery(readouts.towerRaceControlTitles, readouts.towerRaceControlTitle, raceControlBanner.title);
107
116
  }
108
117
  }
109
118
  if (readouts.drs) {
@@ -1,3 +1,5 @@
1
+ export { formatCssColor } from '../../config/cssValues.js';
2
+
1
3
  export function escapeHtml(value) {
2
4
  return String(value ?? '')
3
5
  .replaceAll('&', '&amp;')
@@ -1,20 +1,26 @@
1
- import { setTextAll, setText } from '../domBindings.js';
1
+ import { setStyleProperty, setTextAll, setText } from '../domBindings.js';
2
2
  import { clamp } from '../../simulation/simMath.js';
3
3
  import { formatDriverNumber } from '../../data/championship.js';
4
- import { formatTelemetryGap, formatTelemetryTime, setPerformanceClass } from './readoutFormatters.js';
4
+ import {
5
+ formatCssColor,
6
+ formatTelemetryGap,
7
+ formatTelemetryTime,
8
+ setPerformanceClass,
9
+ } from './readoutFormatters.js';
5
10
 
6
11
  export function renderTelemetryReadouts({ readouts, car, driverById }) {
7
12
  if (!car) return;
8
13
  const driver = driverById.get(car.id);
9
14
  const drsState = car.drsActive ? 'OPEN' : car.drsEligible ? 'READY' : 'OFF';
10
15
  const surface = (car.surface ?? 'track').toUpperCase();
16
+ const driverColor = formatCssColor(car.color);
11
17
 
12
18
  setTextAll(readouts.selectedCode, car.code);
13
19
  readouts.selectedCode?.forEach?.((node) => {
14
- node.style.color = car.color;
20
+ node.style.color = driverColor;
15
21
  });
16
22
  readouts.telemetrySectorBanners?.forEach?.((node) => {
17
- node.style.setProperty('--driver-color', car.color);
23
+ setStyleProperty(node, '--driver-color', driverColor);
18
24
  });
19
25
  setTextAll(readouts.selectedName, car.name);
20
26
  setTextAll(readouts.speed, `${Math.round(car.speedKph)} km/h`);
@@ -69,9 +75,7 @@ export function renderLapTelemetry(readouts, telemetry) {
69
75
  const fill = clamp((progress ?? 0) * 100, 0, 100);
70
76
  const sectorComplete = fill >= 99.9;
71
77
  const fillValue = `${fill.toFixed(1)}%`;
72
- if (bar.style.getPropertyValue('--sector-fill') !== fillValue) {
73
- bar.style.setProperty('--sector-fill', fillValue);
74
- }
78
+ setStyleProperty(bar, '--sector-fill', fillValue);
75
79
  bar.classList.toggle('is-active', isActive);
76
80
  bar.classList.toggle('is-complete', sectorComplete);
77
81
  setPerformanceClass(bar, isCompletedCurrentSector ? telemetry.sectorPerformance?.current?.[index] : null);
@@ -84,11 +88,16 @@ export function renderLapTelemetry(readouts, telemetry) {
84
88
  const isActive = index === activeIndex;
85
89
  const isCompletedCurrentSector = index < activeIndex;
86
90
  const hasLiveSectors = Array.isArray(telemetry.liveSectors);
91
+ const activeSectorCompleted = isActive &&
92
+ Number.isFinite(telemetry.liveSectors?.[index]) &&
93
+ (telemetry.sectorProgress?.[index] ?? 0) >= 0.999;
87
94
  const value = index > activeIndex
88
95
  ? null
89
96
  : hasLiveSectors
90
97
  ? isActive
91
- ? telemetry.currentSectorElapsed ?? telemetry.liveSectors[index]
98
+ ? activeSectorCompleted
99
+ ? telemetry.liveSectors[index]
100
+ : telemetry.currentSectorElapsed ?? telemetry.liveSectors[index]
92
101
  : telemetry.liveSectors[index]
93
102
  : isActive && !Number.isFinite(telemetry.currentSectors?.[index])
94
103
  ? telemetry.currentSectorElapsed