@inventure71/paddockjs 4.2.0 → 4.3.1

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 (249) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +31 -2
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +109 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +31 -15
  9. package/docs/composable-layouts.md +2 -0
  10. package/docs/concepts.md +12 -1
  11. package/docs/custom_model_controller.md +1 -1
  12. package/docs/data_contract.md +33 -10
  13. package/docs/data_model.md +4 -2
  14. package/docs/efficiency_cleanup_2026-10-13.md +80 -0
  15. package/docs/grid_walk.md +84 -0
  16. package/docs/index.md +1 -0
  17. package/docs/keyboard_driving_fix.md +118 -0
  18. package/docs/limit_driving_2026-10-14.md +88 -0
  19. package/docs/maintainability_audit_2026-09-08.md +146 -0
  20. package/docs/racing_ai_improvements.md +145 -0
  21. package/docs/rules.md +43 -13
  22. package/docs/sense_contract.md +4 -1
  23. package/docs/system_specs.md +38 -11
  24. package/docs/testing-reliability-guide.md +5 -0
  25. package/docs/v11_improvements.md +108 -0
  26. package/package.json +24 -13
  27. package/src/api/MountedSimulatorComponents.js +101 -0
  28. package/src/api/PaddockSimulatorController.js +56 -81
  29. package/src/app/BrowserExpertAdapter.js +0 -6
  30. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  31. package/src/app/ExternalTrackSnapshot.js +50 -0
  32. package/src/app/F1SimulatorApp.js +242 -297
  33. package/src/app/HostLifecycleEmitter.js +93 -0
  34. package/src/app/banners/raceDataBannerController.js +9 -4
  35. package/src/app/camera/cameraConstants.js +1 -1
  36. package/src/app/camera/cameraController.js +10 -13
  37. package/src/app/camera/cameraGeometry.js +1 -1
  38. package/src/app/controls/SimulatorViewControls.js +45 -0
  39. package/src/app/controls/workbenchBindings.js +64 -0
  40. package/src/app/controls/workbenchDisclosure.js +41 -0
  41. package/src/app/domBindings.js +21 -13
  42. package/src/app/gridWalk/GridWalkController.js +180 -0
  43. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  44. package/src/app/gridWalk/gridWalkControls.js +24 -0
  45. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  46. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  47. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  48. package/src/app/gridWalk/gridWalkNavigation.js +106 -0
  49. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  50. package/src/app/gridWalk/gridWalkSession.js +50 -0
  51. package/src/app/layoutSupport.js +2 -0
  52. package/src/app/raceOverlayClearanceSupport.js +21 -7
  53. package/src/app/readouts/carOverviewRenderer.js +9 -4
  54. package/src/app/readouts/readoutFormatters.js +1 -8
  55. package/src/app/readouts/telemetryRenderer.js +1 -3
  56. package/src/app/readouts/timingTowerRenderer.js +48 -50
  57. package/src/app/rendering/appAssets.js +8 -10
  58. package/src/app/rendering/carRenderer.js +27 -79
  59. package/src/app/rendering/displayUtils.js +0 -6
  60. package/src/app/rendering/expertSensorRenderer.js +1 -1
  61. package/src/app/rendering/replayGhostRenderer.js +4 -1
  62. package/src/app/rendering/trackRenderer.js +2 -2
  63. package/src/app/runtime/frameLoop.js +18 -17
  64. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  65. package/src/app/runtime/runtimeTiming.js +4 -1
  66. package/src/app/runtime/runtimeVisibility.js +7 -2
  67. package/src/config/cssValues.js +1 -1
  68. package/src/config/defaultOptions.js +12 -173
  69. package/src/config/restartOptions.js +1 -0
  70. package/src/config/runtimeSeed.js +10 -0
  71. package/src/config/simulatorDefaults.js +106 -0
  72. package/src/config/telemetryModules.js +20 -0
  73. package/src/config/theme/cssTokens.js +85 -0
  74. package/src/config/theme/themeCss.js +72 -0
  75. package/src/config/theme/themeInputs.js +155 -0
  76. package/src/config/theme/themeResolver.js +172 -0
  77. package/src/config/theme/themeSelectors.js +69 -0
  78. package/src/config/theme/themeTokens.js +257 -0
  79. package/src/config/theme/themeUtils.js +15 -0
  80. package/src/config/themeOptions.js +14 -830
  81. package/src/config/timingGapMode.js +0 -2
  82. package/src/config/uiOptions.js +53 -0
  83. package/src/data/championship.js +16 -158
  84. package/src/data/championshipDefaults.js +87 -0
  85. package/src/data/driverData.js +10 -20
  86. package/src/data/entryIdentity.js +67 -0
  87. package/src/data/entryPresentation.js +23 -0
  88. package/src/data/entryTypes.d.ts +160 -0
  89. package/src/data/index.d.ts +13 -259
  90. package/src/data/ratingScale.js +14 -0
  91. package/src/data/teamTypes.d.ts +23 -0
  92. package/src/data/trackTypes.d.ts +101 -0
  93. package/src/data/vehicleData.js +12 -23
  94. package/src/environment/actions.js +3 -2
  95. package/src/environment/controllerLoop.js +57 -44
  96. package/src/environment/controllerScheduler.js +30 -0
  97. package/src/environment/driverObservations.js +260 -0
  98. package/src/environment/driverReset.js +51 -0
  99. package/src/environment/episode.js +0 -1
  100. package/src/environment/evaluation.js +6 -90
  101. package/src/environment/evaluationTracker.js +100 -0
  102. package/src/environment/events.js +23 -0
  103. package/src/environment/index.d.ts +21 -140
  104. package/src/environment/metrics.js +3 -17
  105. package/src/environment/observationSchema.js +104 -0
  106. package/src/environment/observationVector.js +16 -115
  107. package/src/environment/observations.js +16 -316
  108. package/src/environment/options.js +5 -24
  109. package/src/environment/renderFrames.js +21 -0
  110. package/src/environment/resetOptions.js +41 -0
  111. package/src/environment/results.js +125 -0
  112. package/src/environment/rewards.js +10 -16
  113. package/src/environment/runtime.js +17 -300
  114. package/src/environment/scenarioOptions.js +90 -0
  115. package/src/environment/scenarioPlacements.js +70 -0
  116. package/src/environment/scenarioPresets.js +78 -0
  117. package/src/environment/scenarios.js +5 -236
  118. package/src/environment/scratch.js +18 -0
  119. package/src/environment/sensorOptions.js +20 -0
  120. package/src/environment/sensors/index.js +2 -556
  121. package/src/environment/sensors/indexedRayBands.js +0 -18
  122. package/src/environment/sensors/nearbyCars.js +18 -15
  123. package/src/environment/sensors/opponentRadar.js +3 -2
  124. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  125. package/src/environment/sensors/rayBandTrace.js +1 -37
  126. package/src/environment/sensors/rayBatch.js +81 -0
  127. package/src/environment/sensors/rayConfig.js +26 -0
  128. package/src/environment/sensors/rayGeometry.js +0 -8
  129. package/src/environment/sensors/rayGuards.js +0 -6
  130. package/src/environment/sensors/rayOutputs.js +75 -0
  131. package/src/environment/sensors/rayQuery.js +110 -0
  132. package/src/environment/sensors/rayScratch.js +146 -0
  133. package/src/environment/sensors/sensorTargets.js +12 -4
  134. package/src/environment/sensors/surfaceRays.js +6 -61
  135. package/src/environment/sensors/trackRays.js +7 -80
  136. package/src/environment/simulationSetup.js +30 -0
  137. package/src/environment/specs.js +15 -109
  138. package/src/index.d.ts +34 -259
  139. package/src/index.js +28 -6
  140. package/src/placeholder/index.js +1 -8
  141. package/src/rendering/displayLifecycle.js +6 -0
  142. package/src/rendering/proceduralTrackAsset.js +1 -1
  143. package/src/rendering/renderSnapshot.js +3 -0
  144. package/src/rendering/track/finishLineRenderer.js +1 -1
  145. package/src/rendering/track/gridRenderer.js +1 -1
  146. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  147. package/src/rendering/track/pitLaneRenderer.js +1 -1
  148. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  149. package/src/rendering/track/trackRenderConstants.js +0 -1
  150. package/src/rendering/track/trackRenderGeometry.js +1 -7
  151. package/src/simulation/collisionGeometry.js +5 -11
  152. package/src/simulation/driver/advancedPathControls.js +53 -0
  153. package/src/simulation/driver/advancedRacePace.js +112 -0
  154. package/src/simulation/driver/advancedRacingControls.js +61 -0
  155. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  156. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  157. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  158. package/src/simulation/driver/arcadeRacePace.js +83 -0
  159. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  160. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  161. package/src/simulation/driver/driverControlConstants.js +0 -2
  162. package/src/simulation/driver/driverController.js +14 -3
  163. package/src/simulation/driver/edgeRecovery.js +0 -1
  164. package/src/simulation/driver/racingControls.js +10 -406
  165. package/src/simulation/driver/racingDriverProfile.js +48 -0
  166. package/src/simulation/driver/racingLinePlan.js +48 -15
  167. package/src/simulation/driver/rejoinControls.js +58 -111
  168. package/src/simulation/driver/safetyCarControls.js +12 -0
  169. package/src/simulation/driver/trafficScan.js +89 -6
  170. package/src/simulation/pit/pitCarPlacement.js +29 -0
  171. package/src/simulation/pit/pitFlow.js +8 -30
  172. package/src/simulation/pit/pitIntent.js +4 -3
  173. package/src/simulation/pit/pitOccupancy.js +2 -2
  174. package/src/simulation/pit/pitQueue.js +2 -18
  175. package/src/simulation/pit/pitRouteMovement.js +1 -36
  176. package/src/simulation/pit/pitRouting.js +5 -81
  177. package/src/simulation/pit/pitService.js +3 -32
  178. package/src/simulation/pit/pitServiceConstants.js +1 -0
  179. package/src/simulation/pit/pitServiceProgress.js +34 -0
  180. package/src/simulation/pit/pitSnapshots.js +8 -4
  181. package/src/simulation/pit/pitState.js +1 -5
  182. package/src/simulation/race/classification.js +1 -8
  183. package/src/simulation/race/raceFinish.js +3 -8
  184. package/src/simulation/race/raceOrder.js +2 -13
  185. package/src/simulation/race/raceProgress.js +6 -20
  186. package/src/simulation/race/raceSetup.js +1 -1
  187. package/src/simulation/race/raceStep.js +1 -1
  188. package/src/simulation/race/redFlag.js +2 -4
  189. package/src/simulation/race/safetyCar.js +3 -8
  190. package/src/simulation/race/stalledDnf.js +2 -4
  191. package/src/simulation/raceSimulation.js +6 -38
  192. package/src/simulation/replay/replayGhosts.js +44 -32
  193. package/src/simulation/rules/collisionSteward.js +0 -14
  194. package/src/simulation/rules/penaltyActions.js +37 -0
  195. package/src/simulation/rules/penaltyLedger.js +8 -1
  196. package/src/simulation/rules/rulesReview.js +1 -7
  197. package/src/simulation/simMath.js +4 -0
  198. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  199. package/src/simulation/timing/drsState.js +8 -0
  200. package/src/simulation/timing/drsTiming.js +2 -4
  201. package/src/simulation/timing/lapTelemetry.js +7 -4
  202. package/src/simulation/timing/sectorPerformance.js +3 -3
  203. package/src/simulation/timing/timingLines.js +12 -6
  204. package/src/simulation/track/pitLaneAccess.js +2 -2
  205. package/src/simulation/track/pitLaneLayout.js +4 -135
  206. package/src/simulation/track/pitLaneModel.js +70 -0
  207. package/src/simulation/track/pitLaneState.js +0 -1
  208. package/src/simulation/track/pitServiceLayout.js +121 -0
  209. package/src/simulation/track/proceduralTrack.js +77 -0
  210. package/src/simulation/track/spatialQueries.js +1 -25
  211. package/src/simulation/track/trackBuilder.js +54 -0
  212. package/src/simulation/track/trackConstants.js +0 -22
  213. package/src/simulation/track/trackMath.js +4 -29
  214. package/src/simulation/track/trackModel.js +4 -241
  215. package/src/simulation/track/trackModelCache.js +105 -0
  216. package/src/simulation/track/trackNearestQueries.js +1218 -0
  217. package/src/simulation/track/trackQueryConstants.js +3 -0
  218. package/src/simulation/track/trackQueryConstruction.js +368 -0
  219. package/src/simulation/track/trackQueryIndex.js +12 -1933
  220. package/src/simulation/track/trackQuerySegments.js +174 -0
  221. package/src/simulation/track/trackQueryStats.js +12 -0
  222. package/src/simulation/track/trackRayQueries.js +165 -0
  223. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  224. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  225. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  226. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  227. package/src/simulation/vehicle/contactResolution.js +20 -6
  228. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  229. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  230. package/src/simulation/vehicle/runoffResponse.js +2 -4
  231. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  232. package/src/simulation/vehicle/vehicleGeometry.js +0 -56
  233. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  234. package/src/simulation/vehicle/vehiclePhysics.js +35 -194
  235. package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
  236. package/src/simulation/vehicle/vehicleState.js +2 -1
  237. package/src/simulation/vehicle/wheelSurface.js +13 -432
  238. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  239. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  240. package/src/styles.css +173 -19
  241. package/src/ui/bannerTemplates.js +4 -2
  242. package/src/ui/cameraControlsTemplate.js +4 -2
  243. package/src/ui/gridWalkTemplate.js +29 -0
  244. package/src/ui/htmlEscaping.js +8 -0
  245. package/src/ui/shellTemplate.js +3 -0
  246. package/src/ui/telemetryDrawerTemplate.js +4 -2
  247. package/src/ui/telemetryTemplates.js +3 -22
  248. package/src/ui/templateUtils.js +2 -8
  249. package/src/ui/viewControlsTemplate.js +21 -0
@@ -0,0 +1,50 @@
1
+ import { getStartGridSlot } from '../../simulation/vehicle/vehicleState.js';
2
+ import { offsetTrackPoint, pointAt } from '../../simulation/track/trackModel.js';
3
+
4
+ // This state owns display poses only. It never steps, resets or writes the race.
5
+ export class GridWalkSession {
6
+ constructor(simulation, options) {
7
+ this.view = options.initialView;
8
+ this.hasRaced = this.view === 'race';
9
+ this.clearing = false;
10
+ this.clearElapsed = 0;
11
+ const snapshot = simulation.snapshot();
12
+ const participants = new Map(simulation.cars.map((car) => [car.id, car]));
13
+ this.snapshot = {
14
+ ...snapshot,
15
+ replayGhosts: [],
16
+ safetyCar: { ...snapshot.safetyCar, deployed: false },
17
+ cars: snapshot.cars.map((car) => {
18
+ const participant = participants.get(car.id);
19
+ // Standing starts use the actual assigned slots, including grid penalties.
20
+ // Rolling starts get display-only staging slots without changing their rules.
21
+ const slot = participant.gridLocked ? participant : getStartGridSlot(participant.index, { standingStart: true });
22
+ const point = pointAt(simulation.track, slot.gridDistance);
23
+ const pose = offsetTrackPoint(point, slot.gridOffset);
24
+ return { ...car, ...pose, heading: point.heading, speed: 0, drsActive: false,
25
+ previousX: pose.x, previousY: pose.y, previousHeading: point.heading,
26
+ gridDistance: slot.gridDistance, gridOffset: slot.gridOffset };
27
+ }).sort((a, b) => b.gridDistance - a.gridDistance),
28
+ };
29
+ }
30
+
31
+ get active() { return this.view === 'grid-walk' || this.clearing; }
32
+
33
+ setView(view, motion) {
34
+ if (!['grid-walk', 'race'].includes(view) || view === this.view) return false;
35
+ this.view = view;
36
+ this.clearing = view === 'race' && !this.hasRaced && motion;
37
+ this.clearElapsed = 0;
38
+ if (view === 'race' && !this.clearing) this.hasRaced = true;
39
+ return true;
40
+ }
41
+
42
+ advance(elapsedMs, motion) {
43
+ if (!this.clearing) return;
44
+ this.clearElapsed += Math.max(0, Math.min(elapsedMs, 100));
45
+ if (!motion || this.clearElapsed >= 650) {
46
+ this.clearing = false;
47
+ this.hasRaced = true;
48
+ }
49
+ }
50
+ }
@@ -5,6 +5,7 @@ export const PADDOCK_MIN_SUPPORTED_INLINE_SIZE = 320;
5
5
  const MIN_INLINE_SIZE_BY_COMPONENT = {
6
6
  'race-controls': 260,
7
7
  'camera-controls': 160,
8
+ 'view-controls': 180,
8
9
  'safety-car-control': 44,
9
10
  'timing-tower': 255,
10
11
  'race-canvas': 180,
@@ -23,6 +24,7 @@ const MIN_INLINE_SIZE_BY_COMPONENT = {
23
24
  const MIN_BLOCK_SIZE_BY_COMPONENT = {
24
25
  'race-controls': 88,
25
26
  'camera-controls': 56,
27
+ 'view-controls': 56,
26
28
  'safety-car-control': 44,
27
29
  'timing-tower': 220,
28
30
  'race-canvas': 180,
@@ -69,19 +69,33 @@ function createPanelOverlayRecord(panel) {
69
69
  };
70
70
  }
71
71
 
72
+ function setPanelClearanceClass(panel, needed) {
73
+ if (panel.classList?.contains(CLEARANCE_CLASS) !== needed) {
74
+ panel.classList?.toggle(CLEARANCE_CLASS, needed);
75
+ }
76
+ }
77
+
78
+ function setPanelClearance(panel, needed, value = '') {
79
+ setPanelClearanceClass(panel, needed);
80
+ // This panel is observed for style changes. Repeating an identical write
81
+ // can notify MutationObserver and schedule another layout pass forever.
82
+ const currentValue = panel.style?.getPropertyValue(CLEARANCE_VAR) ?? '';
83
+ if (currentValue === value) return;
84
+ if (value) panel.style?.setProperty(CLEARANCE_VAR, value);
85
+ else panel.style?.removeProperty(CLEARANCE_VAR);
86
+ }
87
+
72
88
  function syncPanelOverlayClearance(record) {
73
89
  const { panel, tower, banner, toggle } = record;
74
90
  if (!panel.classList?.contains('sim-canvas-panel--responsive-narrow')) {
75
- panel.classList?.remove(CLEARANCE_CLASS);
76
- panel.style?.removeProperty(CLEARANCE_VAR);
91
+ setPanelClearance(panel, false);
77
92
  return;
78
93
  }
79
94
 
80
95
  const toggleStyle = toggle ? getComputedStyle(toggle) : null;
81
96
  const timingRevealMode = Boolean(toggle && toggleStyle?.display !== 'none' && toggleStyle?.visibility !== 'hidden');
82
97
  if (!timingRevealMode || !tower || !isVisible(banner)) {
83
- panel.classList?.remove(CLEARANCE_CLASS);
84
- panel.style?.removeProperty(CLEARANCE_VAR);
98
+ setPanelClearance(panel, false);
85
99
  return;
86
100
  }
87
101
 
@@ -95,16 +109,16 @@ function syncPanelOverlayClearance(record) {
95
109
  const needsClearance = timingEntryBounds(panel, tower)
96
110
  .some((entryBounds) => rectanglesOverlap(entryBounds, bannerWithoutClearance));
97
111
 
98
- panel.classList?.toggle(CLEARANCE_CLASS, needsClearance);
112
+ setPanelClearanceClass(panel, needsClearance);
99
113
  if (!needsClearance) {
100
- panel.style?.removeProperty(CLEARANCE_VAR);
114
+ setPanelClearance(panel, false);
101
115
  return;
102
116
  }
103
117
 
104
118
  const panelRect = panel.getBoundingClientRect();
105
119
  const bannerRect = banner.getBoundingClientRect();
106
120
  const clearance = Math.max(0, Math.ceil(panelRect.bottom - bannerRect.top + OVERLAY_GAP_PX));
107
- panel.style?.setProperty(CLEARANCE_VAR, `${clearance}px`);
121
+ setPanelClearance(panel, true, `${clearance}px`);
108
122
  }
109
123
 
110
124
  function raceCanvasPanels(root) {
@@ -29,10 +29,8 @@ export function getOverviewFields(driver, mode) {
29
29
  label,
30
30
  value: ratings[key] ?? DRIVER_STAT_DEFINITIONS[key]?.neutral,
31
31
  })),
32
- ...normalizeCustomFields([
33
- ...(driver?.constructorArgs?.driver?.customFields ?? []),
34
- ...(driver?.customFields ?? []),
35
- ]),
32
+ ...normalizeCustomFields(driver?.constructorArgs?.driver?.customFields),
33
+ ...normalizeCustomFields(driver?.customFields),
36
34
  ];
37
35
  }
38
36
 
@@ -56,6 +54,13 @@ export function renderCarDriverOverview({
56
54
  lastOverviewRenderKey,
57
55
  }) {
58
56
  if (!car || !readouts.carOverview) return lastOverviewRenderKey;
57
+ if (readouts.carOverviewBindings?.length) {
58
+ for (const binding of readouts.carOverviewBindings) {
59
+ binding.lastOverviewRenderKey = renderCarDriverOverview({ car, driverById, assets, overviewMode, readouts: binding,
60
+ lastOverviewRenderKey: binding.lastOverviewRenderKey });
61
+ }
62
+ return readouts.carOverviewBindings[0].lastOverviewRenderKey;
63
+ }
59
64
  const driver = driverById.get(car.id);
60
65
  const icon = car.icon ?? driver?.icon ?? car.code;
61
66
  const driverNumber = formatDriverNumber(car.driverNumber ?? driver?.driverNumber);
@@ -1,13 +1,6 @@
1
1
  export { formatCssColor } from '../../config/cssValues.js';
2
2
 
3
- export function escapeHtml(value) {
4
- return String(value ?? '')
5
- .replaceAll('&', '&')
6
- .replaceAll('<', '&lt;')
7
- .replaceAll('>', '&gt;')
8
- .replaceAll('"', '&quot;')
9
- .replaceAll("'", '&#39;');
10
- }
3
+ export { escapeHtml } from '../../ui/htmlEscaping.js';
11
4
 
12
5
  export function getTireClass(tire) {
13
6
  return String(tire ?? 'M').toLowerCase();
@@ -1,6 +1,5 @@
1
1
  import { setStyleProperty, setTextAll, setText } from '../domBindings.js';
2
2
  import { clamp } from '../../simulation/simMath.js';
3
- import { formatDriverNumber } from '../../data/championship.js';
4
3
  import {
5
4
  formatCssColor,
6
5
  formatTelemetryGap,
@@ -8,9 +7,8 @@ import {
8
7
  setPerformanceClass,
9
8
  } from './readoutFormatters.js';
10
9
 
11
- export function renderTelemetryReadouts({ readouts, car, driverById }) {
10
+ export function renderTelemetryReadouts({ readouts, car }) {
12
11
  if (!car) return;
13
- const driver = driverById.get(car.id);
14
12
  const drsState = car.drsActive ? 'OPEN' : car.drsEligible ? 'READY' : 'OFF';
15
13
  const surface = (car.surface ?? 'track').toUpperCase();
16
14
  const driverColor = formatCssColor(car.color);
@@ -1,5 +1,6 @@
1
1
  import { getNextTimingGapMode, getTimingGapModeLabel, normalizeTimingGapMode } from '../../config/timingGapMode.js';
2
- import { setText, setTextAll } from '../domBindings.js';
2
+ import { formatPenaltyHeadline } from '../banners/bannerFormatters.js';
3
+ import { setStyleProperty as setDomStyleProperty, setText, setTextAll } from '../domBindings.js';
3
4
  import {
4
5
  escapeHtml,
5
6
  formatCssColor,
@@ -8,29 +9,6 @@ import {
8
9
  getTireClass,
9
10
  } from './readoutFormatters.js';
10
11
 
11
- function formatPenaltyHeadline(penalty) {
12
- const serviceType = penalty?.serviceType;
13
- if (serviceType === 'driveThrough') {
14
- return penalty?.unserved ? 'unserved drive-through penalty' : 'drive-through penalty';
15
- }
16
- if (serviceType === 'stopGo') {
17
- return penalty?.unserved ? 'unserved stop-go penalty' : 'stop-go penalty';
18
- }
19
- const positionDrop = Number(penalty?.positionDrop);
20
- if (Number.isFinite(positionDrop) && positionDrop > 0) {
21
- return `${positionDrop}-place position drop`;
22
- }
23
- const gridDrop = Number(penalty?.gridDrop);
24
- if (Number.isFinite(gridDrop) && gridDrop > 0) {
25
- return `${gridDrop}-place grid drop`;
26
- }
27
- if (penalty?.disqualified) return 'disqualification';
28
- const seconds = Number(penalty?.penaltySeconds);
29
- if (!Number.isFinite(seconds) || seconds <= 0) return 'Penalty decision';
30
- const unit = seconds === 1 ? 'second' : 'seconds';
31
- return `+${seconds} ${unit} time penalty`;
32
- }
33
-
34
12
  function formatPenaltyBadgeLabel(penalty) {
35
13
  const seconds = Number(penalty?.penaltySeconds);
36
14
  const timeLabel = Number.isFinite(seconds) && seconds > 0 ? `${seconds}s ` : '';
@@ -243,52 +221,61 @@ function createTimingRowView(document) {
243
221
  };
244
222
  }
245
223
 
246
- function setStyleProperty(element, name, value) {
224
+ function setTimingStyleProperty(element, name, value) {
247
225
  if (typeof element.style?.setProperty === 'function') {
248
- element.style.setProperty(name, value);
226
+ setDomStyleProperty(element, name, value);
249
227
  } else {
250
- element.setAttribute?.('style', `${name}: ${value}`);
228
+ setAttributeIfChanged(element, 'style', `${name}: ${value}`);
251
229
  }
252
230
  }
253
231
 
232
+ function setAttributeIfChanged(element, name, value) {
233
+ const nextValue = String(value);
234
+ if (element.getAttribute?.(name) !== nextValue) element.setAttribute?.(name, nextValue);
235
+ }
236
+
237
+ function setClassName(element, className) {
238
+ if (element.className !== className) element.className = className;
239
+ }
240
+
254
241
  function updateTimingRowView(view, row) {
255
- view.button.className = `timing-row ${row.selected ? 'is-selected' : ''} ${row.dnf ? 'is-dnf' : ''}`;
256
- view.button.setAttribute('type', 'button');
257
- view.button.setAttribute('data-driver-id', row.id);
258
- view.button.setAttribute('aria-label', `Select ${row.name}`);
259
- setStyleProperty(view.button, '--driver-color', row.driverColor);
242
+ setClassName(view.button, `timing-row ${row.selected ? 'is-selected' : ''} ${row.dnf ? 'is-dnf' : ''}`);
243
+ setAttributeIfChanged(view.button, 'type', 'button');
244
+ setAttributeIfChanged(view.button, 'data-driver-id', row.id);
245
+ setAttributeIfChanged(view.button, 'aria-label', `Select ${row.name}`);
246
+ setTimingStyleProperty(view.button, '--driver-color', row.driverColor);
260
247
 
261
- view.position.className = 'timing-position';
262
- view.position.textContent = String(row.rank);
248
+ setClassName(view.position, 'timing-position');
249
+ setText(view.position, String(row.rank));
263
250
 
264
- view.icon.className = 'timing-icon timing-team-icon';
265
- view.icon.setAttribute('aria-hidden', 'true');
266
- setStyleProperty(view.icon, '--team-color', row.iconColor);
267
- view.icon.textContent = String(row.icon ?? '');
251
+ setClassName(view.icon, 'timing-icon timing-team-icon');
252
+ setAttributeIfChanged(view.icon, 'aria-hidden', 'true');
253
+ setTimingStyleProperty(view.icon, '--team-color', row.iconColor);
254
+ setText(view.icon, String(row.icon ?? ''));
268
255
 
269
- view.name.className = 'timing-name';
270
- view.name.setAttribute('title', row.name);
271
- view.code.textContent = String(row.timingCode ?? '');
256
+ setClassName(view.name, 'timing-name');
257
+ setAttributeIfChanged(view.name, 'title', row.name);
258
+ setText(view.code, String(row.timingCode ?? ''));
272
259
 
273
260
  if (row.penaltyLabel) {
274
261
  if (!view.penaltyBadge) {
275
262
  view.penaltyBadge = view.name.ownerDocument.createElement('span');
276
263
  view.name.appendChild(view.penaltyBadge);
277
264
  }
278
- view.penaltyBadge.className = 'timing-penalty-badge';
279
- view.penaltyBadge.setAttribute('aria-label', row.penaltyLabel);
280
- view.penaltyBadge.setAttribute('title', row.penaltyLabel);
281
- view.penaltyBadge.textContent = '!';
265
+ setClassName(view.penaltyBadge, 'timing-penalty-badge');
266
+ setAttributeIfChanged(view.penaltyBadge, 'aria-label', row.penaltyLabel);
267
+ setAttributeIfChanged(view.penaltyBadge, 'title', row.penaltyLabel);
268
+ setText(view.penaltyBadge, '!');
282
269
  } else if (view.penaltyBadge) {
283
270
  view.penaltyBadge.remove();
284
271
  view.penaltyBadge = null;
285
272
  }
286
273
 
287
- view.gap.className = 'timing-gap';
288
- view.gap.textContent = String(row.gap);
274
+ setClassName(view.gap, 'timing-gap');
275
+ setText(view.gap, String(row.gap));
289
276
 
290
- view.tire.className = `timing-tire timing-tire--${row.tireClass}`;
291
- view.tire.textContent = String(row.tire ?? '');
277
+ setClassName(view.tire, `timing-tire timing-tire--${row.tireClass}`);
278
+ setText(view.tire, String(row.tire ?? ''));
292
279
  }
293
280
 
294
281
  function patchTimingRows(timingList, rows) {
@@ -304,7 +291,6 @@ function patchTimingRows(timingList, rows) {
304
291
  }
305
292
  seen.add(row.id);
306
293
  updateTimingRowView(view, row);
307
- timingList.appendChild(view.item);
308
294
  });
309
295
 
310
296
  rowViews.forEach((view, id) => {
@@ -312,6 +298,18 @@ function patchTimingRows(timingList, rows) {
312
298
  view.item.remove();
313
299
  rowViews.delete(id);
314
300
  });
301
+
302
+ if (typeof timingList.insertBefore === 'function') {
303
+ rows.forEach((row, index) => {
304
+ const item = rowViews.get(row.id).item;
305
+ const currentItem = timingList.children[index] ?? null;
306
+ if (currentItem !== item) timingList.insertBefore(item, currentItem);
307
+ });
308
+ } else if (rows.some((row, index) => timingList.children[index] !== rowViews.get(row.id).item)) {
309
+ // Minimal host/test DOM implementations may only support appendChild.
310
+ // Append in desired order only when reconciliation is actually needed.
311
+ rows.forEach((row) => timingList.appendChild(rowViews.get(row.id).item));
312
+ }
315
313
  }
316
314
 
317
315
  export function renderTimingTower({
@@ -6,33 +6,31 @@ async function loadTexture(url, { configure = null } = {}) {
6
6
  configure?.(texture);
7
7
  return texture;
8
8
  } catch {
9
+ // Unavailable images and incompatible texture sources both use the visible fallback.
9
10
  return Texture.WHITE;
10
11
  }
11
12
  }
12
13
 
13
14
  export async function loadAppTextures(assets) {
14
- const textures = {
15
- car: await loadTexture(assets.car, {
15
+ const [car, safetyCar, trackTextures] = await Promise.all([
16
+ loadTexture(assets.car, {
16
17
  configure(texture) {
17
18
  texture.source.scaleMode = 'linear';
18
19
  texture.source.autoGenerateMipmaps = true;
19
20
  },
20
21
  }),
21
- safetyCar: await loadTexture(assets.safetyCar, {
22
+ loadTexture(assets.safetyCar, {
22
23
  configure(texture) {
23
24
  texture.source.scaleMode = 'linear';
24
25
  texture.source.autoGenerateMipmaps = true;
25
26
  },
26
27
  }),
27
- };
28
-
29
- await Promise.all(Object.entries(assets.trackTextures).map(async ([key, url]) => {
30
- textures[key] = await loadTexture(url, {
28
+ Promise.all(Object.entries(assets.trackTextures).map(async ([key, url]) => [key, await loadTexture(url, {
31
29
  configure(texture) {
32
30
  texture.source.scaleMode = 'linear';
33
31
  },
34
- });
35
- }));
32
+ })])),
33
+ ]);
36
34
 
37
- return textures;
35
+ return { car, safetyCar, ...Object.fromEntries(trackTextures) };
38
36
  }
@@ -1,11 +1,10 @@
1
1
  import { Container, Graphics, Sprite, Text, Texture } from 'pixi.js';
2
2
  import { metersToSimUnits } from '../../simulation/units.js';
3
- import { VEHICLE_GEOMETRY } from '../../simulation/vehicleGeometry.js';
3
+ import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
4
4
  import { CAR_WORLD_LENGTH } from '../camera/cameraConstants.js';
5
5
  import { colorToTint, smoothAngle } from './displayUtils.js';
6
6
 
7
7
  const CAR_WORLD_WIDTH = VEHICLE_GEOMETRY.visualWidth;
8
- const TEMP_RENDER_RAW_CAR_GEOMETRY = false;
9
8
  const NON_COLLIDING_MARKER_COLOR = 0x38bdf8;
10
9
  const SAFETY_CAR_WORLD_LENGTH = metersToSimUnits(5.3);
11
10
  const SAFETY_CAR_WORLD_WIDTH = metersToSimUnits(2.1);
@@ -54,26 +53,20 @@ export class CarRenderer {
54
53
  }
55
54
 
56
55
  drivers.forEach((driver) => {
57
- const sprite = TEMP_RENDER_RAW_CAR_GEOMETRY
58
- ? this.createRawCarGeometryGraphic(driver)
59
- : new Sprite(texture);
60
- if (!TEMP_RENDER_RAW_CAR_GEOMETRY) {
61
- sprite.anchor.set(0.5);
62
- sprite.baseScale = baseScale;
63
- sprite.scale.set(baseScale);
64
- } else {
65
- sprite.baseScale = 1;
66
- }
56
+ const sprite = new Sprite(texture);
57
+ sprite.anchor.set(0.5);
58
+ sprite.baseScale = baseScale;
59
+ sprite.scale.set(baseScale);
67
60
 
68
61
  const marker = this.createNonCollidingMarker();
69
62
  this.nonCollidingMarkers.set(driver.id, marker);
70
63
  carLayer.addChild(marker);
71
64
 
72
- sprite.tint = TEMP_RENDER_RAW_CAR_GEOMETRY ? 0xffffff : colorToTint(driver.color);
65
+ sprite.tint = colorToTint(driver.color);
73
66
  sprite.eventMode = 'static';
74
67
  sprite.cursor = 'pointer';
75
- sprite.on('pointerdown', () => {
76
- this.onSelectCar?.(driver.id);
68
+ sprite.on('pointerdown', (event) => {
69
+ this.onSelectCar?.(driver.id, event);
77
70
  });
78
71
  this.carSprites.set(driver.id, sprite);
79
72
  carLayer.addChild(sprite);
@@ -82,8 +75,8 @@ export class CarRenderer {
82
75
  hit.circle(0, 0, 24).fill({ color: 0xffffff, alpha: 0.001 });
83
76
  hit.eventMode = 'static';
84
77
  hit.cursor = 'pointer';
85
- hit.on('pointerdown', () => {
86
- this.onSelectCar?.(driver.id);
78
+ hit.on('pointerdown', (event) => {
79
+ this.onSelectCar?.(driver.id, event);
87
80
  });
88
81
  this.carHitAreas.set(driver.id, hit);
89
82
  carLayer.addChild(hit);
@@ -95,64 +88,11 @@ export class CarRenderer {
95
88
  });
96
89
  }
97
90
 
98
- createRawCarGeometryGraphic(driver) {
99
- const graphic = new Graphics();
100
- const tint = colorToTint(driver.color);
101
- const wheelLong = VEHICLE_GEOMETRY.wheelLongitudinalOffset;
102
- const wheelLat = VEHICLE_GEOMETRY.wheelLateralOffset;
103
- const wheelLength = VEHICLE_GEOMETRY.wheelLength;
104
- const wheelWidth = VEHICLE_GEOMETRY.wheelWidth;
105
-
106
- graphic
107
- .rect(
108
- -VEHICLE_GEOMETRY.bodyLength / 2,
109
- -VEHICLE_GEOMETRY.bodyWidth / 2,
110
- VEHICLE_GEOMETRY.bodyLength,
111
- VEHICLE_GEOMETRY.bodyWidth,
112
- )
113
- .fill({ color: tint, alpha: 0.42 })
114
- .stroke({ width: 2.2, color: 0xf8fafc, alpha: 0.95 });
115
-
116
- [
117
- [wheelLong, -wheelLat],
118
- [wheelLong, wheelLat],
119
- [-wheelLong, -wheelLat],
120
- [-wheelLong, wheelLat],
121
- ].forEach(([x, y]) => {
122
- graphic
123
- .rect(x - wheelLength / 2, y - wheelWidth / 2, wheelLength, wheelWidth)
124
- .fill({ color: 0x111827, alpha: 0.95 })
125
- .stroke({ width: 1.5, color: tint, alpha: 1 });
126
- });
127
-
128
- const noseMarkerLength = metersToSimUnits(0.75);
129
- graphic
130
- .moveTo(VEHICLE_GEOMETRY.bodyLength / 2 - noseMarkerLength, 0)
131
- .lineTo(VEHICLE_GEOMETRY.bodyLength / 2 + noseMarkerLength, 0)
132
- .stroke({ width: 2, color: 0xf1c65b, alpha: 0.95 });
133
- return graphic;
134
- }
135
-
136
91
  createNonCollidingMarker() {
137
92
  const marker = new Graphics();
138
93
  marker.visible = false;
139
94
  marker.eventMode = 'none';
140
95
  marker.interactionVisualRole = 'non-colliding-marker';
141
- return marker;
142
- }
143
-
144
- renderNonCollidingMarker(car) {
145
- const marker = this.nonCollidingMarkers.get(car.id);
146
- if (!marker) return;
147
- const visible = car.interaction?.collidable === false;
148
- if (marker.visible !== visible) marker.visible = visible;
149
- if (!visible) return;
150
-
151
- if (marker.x !== car.x) marker.x = car.x;
152
- if (marker.y !== car.y) marker.y = car.y;
153
- if (marker.rotation !== car.heading) marker.rotation = car.heading;
154
-
155
- marker.clear();
156
96
  const length = CAR_WORLD_LENGTH * 0.82;
157
97
  const width = CAR_WORLD_WIDTH * 1.32;
158
98
  const halfLength = length / 2;
@@ -173,6 +113,19 @@ export class CarRenderer {
173
113
  .lineTo(longitudinal * (halfLength - segment), lateral * halfWidth)
174
114
  .stroke({ width: 4, color: NON_COLLIDING_MARKER_COLOR, alpha });
175
115
  });
116
+ return marker;
117
+ }
118
+
119
+ renderNonCollidingMarker(car) {
120
+ const marker = this.nonCollidingMarkers.get(car.id);
121
+ if (!marker) return;
122
+ const visible = car.interaction?.collidable === false;
123
+ if (marker.visible !== visible) marker.visible = visible;
124
+ if (!visible) return;
125
+
126
+ if (marker.x !== car.x) marker.x = car.x;
127
+ if (marker.y !== car.y) marker.y = car.y;
128
+ if (marker.rotation !== car.heading) marker.rotation = car.heading;
176
129
  }
177
130
 
178
131
  createServiceCountdownLabel() {
@@ -208,20 +161,15 @@ export class CarRenderer {
208
161
  label.serviceTone = tone;
209
162
  }
210
163
 
211
- renderCars(snapshot, { textures, carLayer }) {
164
+ renderCars(snapshot, { textures, carLayer, snapToPose = false }) {
212
165
  snapshot.cars.forEach((car) => {
213
166
  const sprite = this.carSprites.get(car.id);
214
167
  const hit = this.carHitAreas.get(car.id);
215
168
  if (!sprite || !hit) return;
216
169
  if (sprite.x !== car.x) sprite.x = car.x;
217
170
  if (sprite.y !== car.y) sprite.y = car.y;
218
- if (TEMP_RENDER_RAW_CAR_GEOMETRY) {
219
- sprite.currentRotation = car.heading;
220
- if (sprite.rotation !== car.heading) sprite.rotation = car.heading;
221
- } else {
222
- sprite.currentRotation = smoothAngle(sprite.currentRotation, car.heading, 0.24);
223
- if (sprite.rotation !== sprite.currentRotation) sprite.rotation = sprite.currentRotation;
224
- }
171
+ sprite.currentRotation = snapToPose ? car.heading : smoothAngle(sprite.currentRotation, car.heading, 0.24);
172
+ if (sprite.rotation !== sprite.currentRotation) sprite.rotation = sprite.currentRotation;
225
173
  const retired = Boolean(car.destroyed || car.dnf || car.outOfRace);
226
174
  const alpha = retired ? 0.48 : snapshot.raceControl.mode === 'safety-car' ? 0.82 : 1;
227
175
  if (sprite.alpha !== alpha) sprite.alpha = alpha;
@@ -229,7 +177,7 @@ export class CarRenderer {
229
177
  sprite.scale.set(sprite.baseScale);
230
178
  sprite.lastRenderedScale = sprite.baseScale;
231
179
  }
232
- const tint = retired ? 0x1f2937 : TEMP_RENDER_RAW_CAR_GEOMETRY ? 0xffffff : colorToTint(car.color);
180
+ const tint = retired ? 0x1f2937 : colorToTint(car.color);
233
181
  if (sprite.tint !== tint) sprite.tint = tint;
234
182
  if (hit.x !== car.x) hit.x = car.x;
235
183
  if (hit.y !== car.y) hit.y = car.y;
@@ -14,9 +14,3 @@ export function smoothAngle(current, target, amount) {
14
14
  if (diff < -Math.PI) diff += Math.PI * 2;
15
15
  return current + diff * amount;
16
16
  }
17
-
18
- export function destroyDisplayChildren(container) {
19
- container?.removeChildren?.().forEach((child) => {
20
- child.destroy?.({ children: true, texture: false, textureSource: false });
21
- });
22
- }
@@ -1,4 +1,4 @@
1
- import { getCarRayOrigin, getCarRayVector } from '../../environment/sensors.js';
1
+ import { getCarRayOrigin, getCarRayVector } from '../../environment/sensors/index.js';
2
2
  import { metersToSimUnits } from '../../simulation/units.js';
3
3
 
4
4
  const SENSOR_RAY_TRACK_COLOR = 0xf1c65b;
@@ -1,6 +1,6 @@
1
1
  import { Graphics, Sprite, Text, Texture } from 'pixi.js';
2
2
  import { CAR_WORLD_LENGTH } from '../camera/cameraConstants.js';
3
- import { VEHICLE_GEOMETRY } from '../../simulation/vehicleGeometry.js';
3
+ import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
4
4
  import { colorToTint, smoothAngle } from './displayUtils.js';
5
5
 
6
6
  const CAR_WORLD_WIDTH = VEHICLE_GEOMETRY.visualWidth;
@@ -111,6 +111,9 @@ export class ReplayGhostRenderer {
111
111
  drawHalo(halo, ghost) {
112
112
  const tint = colorToTint(ghost.color);
113
113
  const alpha = Math.max(0.18, Math.min(0.55, ghost.opacity + 0.16));
114
+ if (halo.lastRenderedTint === tint && halo.lastRenderedAlpha === alpha) return;
115
+ halo.lastRenderedTint = tint;
116
+ halo.lastRenderedAlpha = alpha;
114
117
  halo.clear();
115
118
  halo
116
119
  .ellipse(0, 0, CAR_WORLD_LENGTH * 0.72, CAR_WORLD_WIDTH * 1.25)
@@ -1,6 +1,6 @@
1
1
  import { Graphics } from 'pixi.js';
2
- import { offsetTrackPoint, pointAt } from '../../simulation/trackModel.js';
3
- import { destroyDisplayChildren } from './displayUtils.js';
2
+ import { offsetTrackPoint, pointAt } from '../../simulation/track/trackModel.js';
3
+ import { destroyDisplayChildren } from '../../rendering/displayLifecycle.js';
4
4
 
5
5
  export function renderTrackSurface({ drsLayer, sensorLayer, pitLaneStatusLayer, trackAsset, snapshot }) {
6
6
  destroyDisplayChildren(drsLayer);