@inventure71/paddockjs 4.2.0 → 4.3.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 (249) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/INSTALL_AND_UPDATE.md +23 -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 +80 -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 +36 -11
  24. package/docs/testing-reliability-guide.md +5 -0
  25. package/docs/v11_improvements.md +108 -0
  26. package/package.json +23 -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 +99 -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
package/src/styles.css CHANGED
@@ -552,7 +552,7 @@
552
552
  .f1-sim-component h1 {
553
553
  margin: 0;
554
554
  font-size: clamp(1.8rem, 4vw, 4.6rem);
555
- line-height: 0.88;
555
+ line-height: 1.15;
556
556
  text-transform: uppercase;
557
557
  letter-spacing: 0;
558
558
  }
@@ -580,11 +580,6 @@
580
580
  grid-template-columns: repeat(2, minmax(0, 1fr));
581
581
  }
582
582
 
583
- :where(.f1-sim-component, .f1-sim-component *).sim-topbar h1 {
584
- line-height: 0.92;
585
- min-height: 1.08em;
586
- }
587
-
588
583
  :where(.f1-sim-component, .f1-sim-component *).sim-control,
589
584
  :where(.f1-sim-component, .f1-sim-component *).sim-backlink {
590
585
  text-align: center;
@@ -1426,6 +1421,7 @@
1426
1421
  width: 100% !important;
1427
1422
  height: 100% !important;
1428
1423
  display: block;
1424
+ touch-action: auto;
1429
1425
  }
1430
1426
 
1431
1427
  :where(.f1-sim-component, .f1-sim-component *).physics-mode-indicator {
@@ -1532,7 +1528,7 @@
1532
1528
  position: relative;
1533
1529
  z-index: 13;
1534
1530
  display: flex;
1535
- align-items: center;
1531
+ align-items: stretch;
1536
1532
  justify-content: space-between;
1537
1533
  gap: 0.7rem;
1538
1534
  min-height: var(--race-toolbar-height);
@@ -1545,6 +1541,10 @@
1545
1541
  flex: 1 1 auto;
1546
1542
  }
1547
1543
 
1544
+ :where(.f1-sim-component, .f1-sim-component *).race-telemetry-drawer__toolbar [data-paddock-component='camera-controls'] button {
1545
+ flex: 1 1 auto;
1546
+ }
1547
+
1548
1548
  :where(.f1-sim-component, .f1-sim-component *).race-telemetry-drawer__race {
1549
1549
  position: relative;
1550
1550
  min-width: 0;
@@ -1569,8 +1569,9 @@
1569
1569
 
1570
1570
  :where(.f1-sim-component, .f1-sim-component *).race-telemetry-drawer__controls {
1571
1571
  min-width: 0;
1572
- display: flex;
1573
- flex-wrap: wrap;
1572
+ flex: 0 0 auto;
1573
+ display: grid;
1574
+ grid-template-columns: repeat(2, max-content);
1574
1575
  justify-content: flex-end;
1575
1576
  gap: 0.45rem;
1576
1577
  }
@@ -1680,6 +1681,14 @@
1680
1681
  border-color: var(--text);
1681
1682
  }
1682
1683
 
1684
+ :where(.f1-sim-component, .f1-sim-component *).camera-controls__zoom {
1685
+ display: flex;
1686
+ gap: 0.35rem;
1687
+ flex: 2 1 auto;
1688
+ min-width: calc(88px + 0.35rem);
1689
+ }
1690
+ :where(.f1-sim-component, .f1-sim-component *).camera-controls__zoom button { flex: 1 1 44px; }
1691
+
1683
1692
  :where(.f1-sim-component, .f1-sim-component *).steward-message {
1684
1693
  --steward-panel-bg: rgba(8, 9, 11, 0.92);
1685
1694
  --steward-chip-bg: color-mix(in srgb, var(--steward-color, var(--yellow)) 18%, rgba(8, 9, 11, 0.94));
@@ -3592,7 +3601,7 @@
3592
3601
  }
3593
3602
  }
3594
3603
 
3595
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone {
3604
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone {
3596
3605
  position: relative;
3597
3606
  left: auto;
3598
3607
  right: auto;
@@ -3604,29 +3613,29 @@
3604
3613
  min-height: 0;
3605
3614
  }
3606
3615
 
3607
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone .race-data-copy {
3616
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone .race-data-copy {
3608
3617
  grid-column: 2;
3609
3618
  grid-row: 1;
3610
3619
  min-width: 0;
3611
3620
  padding: 0.75rem 0.68rem 0.55rem;
3612
3621
  }
3613
3622
 
3614
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone .race-data-kicker {
3623
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone .race-data-kicker {
3615
3624
  font-size: 0.54rem;
3616
3625
  letter-spacing: 0.12em;
3617
3626
  }
3618
3627
 
3619
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone .race-data-copy strong {
3628
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone .race-data-copy strong {
3620
3629
  font-size: clamp(1.05rem, 3vw, 1.45rem);
3621
3630
  line-height: 0.98;
3622
3631
  }
3623
3632
 
3624
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone .race-data-subtitle {
3633
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone .race-data-subtitle {
3625
3634
  font-size: 0.55rem;
3626
3635
  letter-spacing: 0.06em;
3627
3636
  }
3628
3637
 
3629
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone .race-data-number {
3638
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone .race-data-number {
3630
3639
  grid-column: 1;
3631
3640
  grid-row: 1;
3632
3641
  min-width: 0;
@@ -3634,7 +3643,7 @@
3634
3643
  font-size: clamp(2.3rem, 5vw, 3.9rem);
3635
3644
  }
3636
3645
 
3637
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone .race-data-link {
3646
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone .race-data-link {
3638
3647
  grid-column: 1 / -1;
3639
3648
  grid-row: 2;
3640
3649
  margin: 0;
@@ -3643,18 +3652,163 @@
3643
3652
  font-size: 0.62rem;
3644
3653
  }
3645
3654
 
3646
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone.race-data-panel--with-telemetry {
3655
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone.race-data-panel--with-telemetry {
3647
3656
  grid-template-columns: minmax(4.6rem, 0.35fr) minmax(0, 1fr);
3648
3657
  }
3649
3658
 
3650
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone.race-data-panel--with-telemetry .race-data-copy {
3659
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone.race-data-panel--with-telemetry .race-data-copy {
3651
3660
  grid-column: 2;
3652
3661
  padding-bottom: 0.35rem;
3653
3662
  }
3654
3663
 
3655
- :where(.f1-sim-component, .f1-sim-component *).race-data-panel--standalone .race-data-telemetry {
3664
+ :where(.f1-sim-component, .f1-sim-component *).race-data-panel.race-data-panel--standalone .race-data-telemetry {
3656
3665
  grid-column: 1 / -1;
3657
3666
  grid-row: auto;
3658
3667
  padding: 0 0.68rem 0.65rem;
3659
3668
  border-left: 0;
3660
3669
  }
3670
+
3671
+ /* Grid Walk positions shared simulator controls over the existing world. */
3672
+ :where(.f1-sim-component, .f1-sim-component *) .grid-walk {
3673
+ position: absolute;
3674
+ inset: 0;
3675
+ z-index: 24;
3676
+ overflow: hidden;
3677
+ pointer-events: none;
3678
+ color: var(--text);
3679
+ }
3680
+ .grid-walk [hidden] { display: none !important; }
3681
+ .is-grid-walk-covered { visibility: hidden !important; pointer-events: none !important; }
3682
+ .grid-walk button, .grid-walk select, .grid-walk a { pointer-events: auto; touch-action: manipulation; }
3683
+ .grid-walk :is(button, select, a, [data-grid-heading]):focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
3684
+ .grid-walk .grid-walk__toolbar {
3685
+ position: absolute;
3686
+ top: 12px;
3687
+ left: 12px;
3688
+ right: 12px;
3689
+ display: flex;
3690
+ flex-wrap: wrap;
3691
+ align-items: flex-start;
3692
+ justify-content: center;
3693
+ gap: 8px;
3694
+ }
3695
+ .grid-walk .grid-walk__toolbar > .camera-controls {
3696
+ position: static;
3697
+ max-width: 100%;
3698
+ box-sizing: border-box;
3699
+ background: var(--paddock-camera-controls-background, var(--surface-panel));
3700
+ }
3701
+ :where(.f1-sim-component, .f1-sim-component *).paddock-view-controls[hidden] { display: none !important; }
3702
+ .paddock-view-controls [hidden] { display: none !important; }
3703
+ :where(.f1-sim-component, .f1-sim-component *).paddock-view-controls { flex: 0 0 auto; }
3704
+ .race-telemetry-drawer__toolbar .paddock-view-controls { flex: 0 0 auto; }
3705
+ .paddock-view-controls button:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
3706
+ .paddock-view-controls.camera-controls button[data-grid-action='start'] {
3707
+ color: #08090b;
3708
+ background: var(--green);
3709
+ border-color: var(--green);
3710
+ }
3711
+ .paddock-view-controls.camera-controls button[data-grid-action='stop'] {
3712
+ color: #ffffff;
3713
+ background: var(--race-control-red);
3714
+ border-color: var(--race-control-red);
3715
+ }
3716
+ /* Intrinsic sizing reserves the longest action label in both states. */
3717
+ :where(.f1-sim-component, .f1-sim-component *) button[data-size-label] { display: grid; place-items: center; }
3718
+ :where(.f1-sim-component, .f1-sim-component *) button[data-size-label]::after { content: attr(data-size-label); visibility: hidden; grid-area: 1 / 1; }
3719
+ :where(.f1-sim-component, .f1-sim-component *) button[data-size-label] > [data-control-label] { grid-area: 1 / 1; }
3720
+ .grid-walk .grid-walk__entry {
3721
+ position: absolute;
3722
+ top: 0;
3723
+ left: 0;
3724
+ width: 180px;
3725
+ min-height: 44px;
3726
+ padding: 8px 10px;
3727
+ box-sizing: border-box;
3728
+ text-align: left;
3729
+ white-space: normal;
3730
+ font-family: var(--font-display);
3731
+ color: var(--text);
3732
+ background: var(--surface-panel);
3733
+ border: 1px solid var(--line);
3734
+ border-inline-start: 3px solid var(--driver-color, var(--red));
3735
+ box-shadow: 0 3px 12px #0003;
3736
+ cursor: pointer;
3737
+ }
3738
+ .grid-walk .grid-walk__entry:hover { background: var(--surface-raised); border-color: var(--text); }
3739
+ .grid-walk .grid-walk__entry { touch-action: none; }
3740
+ .grid-walk .grid-walk__entry[aria-pressed='true'] {
3741
+ border-color: var(--driver-color, var(--red));
3742
+ box-shadow: inset 0 0 0 1px var(--driver-color, var(--red)), 0 3px 12px #0003;
3743
+ }
3744
+ .grid-walk__entry strong {
3745
+ display: -webkit-box;
3746
+ -webkit-box-orient: vertical;
3747
+ -webkit-line-clamp: 3;
3748
+ overflow: hidden;
3749
+ font-size: .78rem;
3750
+ line-height: 1.25;
3751
+ overflow-wrap: anywhere;
3752
+ }
3753
+ .grid-walk__position { display: block; font: 10px var(--font-mono); color: var(--muted); }
3754
+ .grid-walk[data-compact-labels='true'] .grid-walk__entry { padding: 5px 8px; }
3755
+ .grid-walk[data-compact-labels='true'] .grid-walk__entry strong { -webkit-line-clamp: 1; }
3756
+ .grid-walk__connectors { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; stroke: var(--muted); stroke-width: 1; }
3757
+ .grid-walk .grid-walk__toolbar > .grid-walk__navigation {
3758
+ width: 360px;
3759
+ min-width: min(240px, 100%);
3760
+ max-width: min(360px, 100%);
3761
+ flex: 0 1 360px;
3762
+ flex-wrap: nowrap;
3763
+ align-items: center;
3764
+ }
3765
+ .grid-walk[data-compact-controls='true'] .grid-walk__toolbar > .grid-walk__navigation { min-width: min(180px, 100%); flex: 1 1 180px; }
3766
+ .grid-walk__navigation button { flex: 0 0 44px; }
3767
+ .grid-walk__navigation label { min-width: 0; flex: 1; }
3768
+ .grid-walk .grid-walk__navigation select { display: block; width: 100%; min-height: 44px; font-family: var(--font-mono); padding: 6px; background: var(--surface-panel); text-align: center; text-align-last: center; }
3769
+ .grid-walk[data-view] [data-grid-project-banner] {
3770
+ position: absolute;
3771
+ left: 50%;
3772
+ bottom: 12px;
3773
+ width: calc(100% - 24px);
3774
+ max-width: 1080px;
3775
+ transform: translateX(-50%);
3776
+ pointer-events: auto;
3777
+ min-height: 0;
3778
+ overflow: auto;
3779
+ }
3780
+ .grid-walk__status, .grid-walk__sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
3781
+ .sim-canvas-panel[data-grid-walk-active='true'] [data-track-canvas] canvas { touch-action: none; cursor: grab !important; }
3782
+ .sim-canvas-panel[data-grid-walk-active='true'] [data-grid-dragging='true'] canvas { cursor: grabbing !important; }
3783
+ .sim-canvas-panel[data-grid-walk-active='true'] > :is(.sim-timing, .timing-panel-toggle, .fps-counter, .start-lights, .steward-message, .camera-controls, .race-data-panel, .telemetry-sector-banner, .race-finish-panel) { display: none !important; }
3784
+ @media (prefers-reduced-motion: reduce) { .grid-walk .race-data-panel { transition: none; } }
3785
+
3786
+ /* Opt-in shell chrome uses its container, including embeds on wide pages.
3787
+ This reuses the shell's stacked composition without changing board sizing. */
3788
+ @container (max-width: 900px) {
3789
+ .sim-shell:has(.grid-walk) .sim-grid { grid-template-columns: minmax(0, 1fr); }
3790
+ .sim-shell:has(.grid-walk) .sim-canvas-panel {
3791
+ grid-column: 1;
3792
+ grid-row: 1;
3793
+ min-height: var(--paddock-race-view-min-height, 620px);
3794
+ }
3795
+ .sim-shell:has(.grid-walk) .sim-grid > .sim-timing {
3796
+ position: relative;
3797
+ inset: auto;
3798
+ grid-column: 1;
3799
+ grid-row: 2;
3800
+ width: min(100%, var(--timing-board-max-width));
3801
+ max-width: var(--timing-board-max-width);
3802
+ height: auto;
3803
+ clip-path: none;
3804
+ }
3805
+ .sim-shell:has(.grid-walk) .telemetry-stack { grid-column: 1; grid-row: 3; max-height: none; }
3806
+ .sim-shell:has(.grid-walk) .sim-canvas-panel > .camera-controls { left: 1rem; max-width: calc(100% - 2rem); }
3807
+ .sim-shell:has(.grid-walk) .start-lights { left: 50%; }
3808
+ .sim-shell:has(.grid-walk) .race-data-panel {
3809
+ --race-data-safe-left: 0px;
3810
+ --race-data-banner-left: 50%;
3811
+ --race-data-banner-width: calc(100% - 1.5rem);
3812
+ left: 50%;
3813
+ }
3814
+ }
@@ -13,7 +13,7 @@ export function createStewardMessageMarkup() {
13
13
  `;
14
14
  }
15
15
 
16
- export function createRaceDataPanelMarkup({ ui = {}, standalone = false } = {}) {
16
+ export function createRaceDataPanelMarkup({ ui = {}, standalone = false, navigationLink = false } = {}) {
17
17
  const sizeMode = ui.raceDataBannerSize === 'auto' ? 'auto' : 'custom';
18
18
  const telemetryDetail = Boolean(ui.raceDataTelemetryDetail);
19
19
  const classNames = ['race-data-panel', `race-data-panel--${sizeMode}`];
@@ -28,7 +28,9 @@ export function createRaceDataPanelMarkup({ ui = {}, standalone = false } = {})
28
28
  </div>
29
29
  ${telemetryDetail ? createRaceDataTelemetryMarkup() : ''}
30
30
  <strong class="race-data-number" data-race-data-number>--</strong>
31
- <button class="race-data-link" type="button" data-race-data-open>Open project</button>
31
+ ${navigationLink
32
+ ? '<a class="race-data-link" data-race-data-open>Open project</a>'
33
+ : '<button class="race-data-link" type="button" data-race-data-open>Open project</button>'}
32
34
  `;
33
35
 
34
36
  return createComponentSurfaceMarkup({
@@ -13,8 +13,10 @@ export function createCameraControlsMarkup({ embedded = false, showSimulationSpe
13
13
  ${driverCameraControl}
14
14
  <button type="button" data-camera-mode="show-all" aria-pressed="false">Show all</button>
15
15
  <button type="button" data-camera-mode="pit" aria-pressed="false">Pits</button>
16
- <button type="button" data-zoom-out aria-label="Zoom out">-</button>
17
- <button type="button" data-zoom-in aria-label="Zoom in">+</button>
16
+ <div class="camera-controls__zoom" role="group" aria-label="Zoom">
17
+ <button type="button" data-zoom-out aria-label="Zoom out">-</button>
18
+ <button type="button" data-zoom-in aria-label="Zoom in">+</button>
19
+ </div>
18
20
  ${showSpeedControl ? '<button type="button" data-simulation-speed aria-label="Simulation speed">1x</button>' : ''}
19
21
  <button type="button" data-race-data-banners-muted aria-pressed="false">Mute banners</button>
20
22
  `;
@@ -0,0 +1,29 @@
1
+ import { createRaceDataPanelMarkup } from './bannerTemplates.js';
2
+ import { escapeHtml as html } from './htmlEscaping.js';
3
+
4
+ export function createGridWalkMarkup(drivers, cars, ui = {}) {
5
+ const byId = new Map(drivers.map((driver) => [driver.id, driver]));
6
+ return `<div class="grid-walk" data-grid-walk>
7
+ <div class="grid-walk__toolbar" data-grid-header>
8
+ <div class="camera-controls grid-walk__navigation" data-grid-navigation>
9
+ <button type="button" data-grid-previous aria-label="Previous grid entry">←</button>
10
+ <label><span class="grid-walk__sr-only">Explore the grid</span><select class="sim-control" data-grid-jump aria-label="Jump to grid entry">
11
+ ${cars.map((car, index) => `<option value="${html(car.id)}">${index + 1}. ${html(byId.get(car.id).presentation.name)}</option>`).join('')}
12
+ </select></label>
13
+ <button type="button" data-grid-next aria-label="Next grid entry">→</button>
14
+ </div>
15
+ </div>
16
+ <div class="grid-walk__labels" data-grid-labels role="group" aria-label="Starting grid">
17
+ <svg class="grid-walk__connectors" aria-hidden="true">${cars.map((car) => `<line data-grid-connector="${html(car.id)}" />`).join('')}</svg>
18
+ ${cars.map((car, index) => {
19
+ const entry = byId.get(car.id).presentation;
20
+ return `<button type="button" class="grid-walk__entry" data-grid-entry="${html(car.id)}" title="${html(entry.name)}" style="--driver-color: ${html(byId.get(car.id).color)}" aria-pressed="false">
21
+ <span class="grid-walk__position">${String(index + 1).padStart(2, '0')}</span>
22
+ <strong>${html(entry.name)}</strong>
23
+ </button>`;
24
+ }).join('')}
25
+ </div>
26
+ ${createRaceDataPanelMarkup({ ui: { ...ui, raceDataTelemetryDetail: false }, navigationLink: true })}
27
+ <span class="grid-walk__status" data-grid-status role="status"></span>
28
+ </div>`;
29
+ }
@@ -0,0 +1,8 @@
1
+ export function escapeHtml(value) {
2
+ return String(value ?? '')
3
+ .replaceAll('&', '&amp;')
4
+ .replaceAll('<', '&lt;')
5
+ .replaceAll('>', '&gt;')
6
+ .replaceAll('"', '&quot;')
7
+ .replaceAll("'", '&#39;');
8
+ }
@@ -5,6 +5,7 @@ import {
5
5
  createTelemetryPanelMarkup,
6
6
  createTimingTowerMarkup,
7
7
  } from './componentTemplates.js';
8
+ import { createViewControlsMarkup } from './viewControlsTemplate.js';
8
9
  import { createUnsupportedSizeMarkup } from './templateUtils.js';
9
10
 
10
11
  export function createF1SimulatorShell({
@@ -17,6 +18,7 @@ export function createF1SimulatorShell({
17
18
  physicsMode,
18
19
  assets,
19
20
  ui = {},
21
+ gridWalk = {},
20
22
  }) {
21
23
  const layoutPreset = ui.layoutPreset === 'left-tower-overlay' ? 'left-tower-overlay' : 'standard';
22
24
  const timingFit = ui.timingTowerVerticalFit === 'scroll' ? 'scroll' : 'expand-race-view';
@@ -34,6 +36,7 @@ export function createF1SimulatorShell({
34
36
  <section class="sim-workspace" aria-label="F1 race simulator">
35
37
  ${createRaceControlsMarkup({ title, kicker, backLinkHref, backLinkLabel, showBackLink })}
36
38
  ${cameraControls}
39
+ ${createViewControlsMarkup({ gridWalk, ui })}
37
40
  <div class="sim-grid">
38
41
  ${createTimingTowerMarkup({ totalLaps, assets, ui })}
39
42
  ${createRaceCanvasMarkup({ includeRaceDataPanel: ui.showRaceDataPanel !== false, assets, totalLaps, physicsMode, ui })}
@@ -1,3 +1,4 @@
1
+ import { createViewControlsMarkup } from './viewControlsTemplate.js';
1
2
  import { createCameraControlsMarkup } from './cameraControlsTemplate.js';
2
3
  import { createRaceCanvasMarkup } from './raceCanvasTemplate.js';
3
4
  import { createSafetyCarControlMarkup } from './raceControlsTemplate.js';
@@ -33,11 +34,12 @@ export function createRaceTelemetryDrawerMarkup(options, {
33
34
  const showCameraControls = options.ui?.cameraControls !== false;
34
35
  const body = `
35
36
  <div class="race-telemetry-drawer__toolbar" aria-label="Race workbench controls">
37
+ ${createViewControlsMarkup(options)}
36
38
  ${showCameraControls ? createCameraControlsMarkup({ showSimulationSpeed: true, ui: options.ui }) : ''}
37
39
  <div class="race-telemetry-drawer__controls">
38
40
  ${createSafetyCarControlMarkup({ compact: true })}
39
- <button class="telemetry-drawer-toggle" type="button" data-telemetry-drawer-toggle aria-expanded="${drawerInitiallyOpen ? 'true' : 'false'}" aria-controls="${drawerId}" aria-label="${drawerInitiallyOpen ? 'Close telemetry' : 'Open telemetry'}">
40
- ${drawerInitiallyOpen ? 'Close' : 'Telemetry'}
41
+ <button class="telemetry-drawer-toggle" type="button" data-telemetry-drawer-toggle data-size-label="Telemetry" aria-expanded="${drawerInitiallyOpen ? 'true' : 'false'}" aria-controls="${drawerId}" aria-label="${drawerInitiallyOpen ? 'Close telemetry' : 'Open telemetry'}">
42
+ <span data-control-label>${drawerInitiallyOpen ? 'Close' : 'Telemetry'}</span>
41
43
  </button>
42
44
  </div>
43
45
  </div>
@@ -1,29 +1,10 @@
1
+ import { normalizeTelemetryModules } from '../config/telemetryModules.js';
1
2
  import { createCarDriverOverviewMarkup } from './carOverviewTemplate.js';
2
3
  import {
3
4
  createComponentSurfaceMarkup,
4
5
  createTelemetrySectorBarsMarkup,
5
6
  } from './templateUtils.js';
6
7
 
7
- function getTelemetryModules(ui = {}) {
8
- const defaults = {
9
- core: true,
10
- sectors: true,
11
- lapTimes: true,
12
- sectorTimes: true,
13
- };
14
- const modules = ui.telemetryModules;
15
- if (modules === false) return Object.fromEntries(Object.keys(defaults).map((name) => [name, false]));
16
- if (Array.isArray(modules)) {
17
- const requested = new Set(modules);
18
- return Object.fromEntries(Object.keys(defaults).map((name) => [name, requested.has(name)]));
19
- }
20
- if (!modules || typeof modules !== 'object') return defaults;
21
- return Object.fromEntries(Object.entries(defaults).map(([name, fallback]) => [
22
- name,
23
- modules[name] == null ? fallback : Boolean(modules[name]),
24
- ]));
25
- }
26
-
27
8
  function getTelemetryModuleClass(componentName) {
28
9
  return `sim-telemetry telemetry-component telemetry-component--${componentName}`;
29
10
  }
@@ -135,7 +116,7 @@ export function createTelemetrySectorTimesMarkup() {
135
116
  });
136
117
  }
137
118
 
138
- function createTelemetryComponentMarkup(options, modules = getTelemetryModules(options.ui)) {
119
+ function createTelemetryComponentMarkup(options, modules = normalizeTelemetryModules(options.ui?.telemetryModules)) {
139
120
  return `
140
121
  ${modules.core ? createTelemetryCoreMarkup(options) : ''}
141
122
  ${modules.sectors ? createTelemetrySectorsMarkup(options) : ''}
@@ -145,7 +126,7 @@ function createTelemetryComponentMarkup(options, modules = getTelemetryModules(o
145
126
  }
146
127
 
147
128
  export function createTelemetryPanelMarkup(options, { includeOverview = options.ui?.telemetryIncludesOverview !== false } = {}) {
148
- const modules = getTelemetryModules(options.ui);
129
+ const modules = normalizeTelemetryModules(options.ui?.telemetryModules);
149
130
  const body = `
150
131
  ${createTelemetryComponentMarkup(options, modules)}
151
132
  ${includeOverview ? createCarDriverOverviewMarkup(options) : ''}
@@ -1,11 +1,5 @@
1
- export function escapeHtml(value) {
2
- return String(value ?? '')
3
- .replaceAll('&', '&amp;')
4
- .replaceAll('<', '&lt;')
5
- .replaceAll('>', '&gt;')
6
- .replaceAll('"', '&quot;')
7
- .replaceAll("'", '&#39;');
8
- }
1
+ import { escapeHtml } from './htmlEscaping.js';
2
+ export { escapeHtml } from './htmlEscaping.js';
9
3
 
10
4
  export function createLoadingMarkup(label = 'Loading') {
11
5
  return `
@@ -0,0 +1,21 @@
1
+ import { createComponentSurfaceMarkup } from './templateUtils.js';
2
+
3
+ export function createViewControlsMarkup({ gridWalk = {}, ui = {} } = {}) {
4
+ const enabled = gridWalk.enabled === true;
5
+ const view = enabled && gridWalk.initialView === 'grid-walk' ? 'grid-walk' : 'race';
6
+ const toggle = ui.viewControls === 'toggle';
7
+ return createComponentSurfaceMarkup({
8
+ tagName: 'div',
9
+ className: 'camera-controls camera-controls--external paddock-view-controls',
10
+ componentName: 'view-controls',
11
+ ariaLabel: 'Simulator view',
12
+ attributes: `role="group"${enabled ? '' : ' hidden'}`,
13
+ body: `
14
+ <button type="button" data-grid-view="grid-walk" aria-pressed="${view === 'grid-walk'}" disabled${toggle ? ' hidden' : ''}>Grid Walk</button>
15
+ <button type="button" data-grid-view="race" aria-pressed="${view === 'race'}" disabled${toggle ? ' hidden' : ''}>Race</button>
16
+ <button type="button" data-grid-action="${view === 'grid-walk' ? 'start' : 'stop'}" data-grid-toggle data-size-label="Start race" disabled${toggle ? '' : ' hidden'}><span data-control-label>${view === 'grid-walk' ? 'Start race' : 'Stop race'}</span></button>
17
+ `,
18
+ unsupportedLabel: 'Simulator view',
19
+ loadingLabel: 'Simulator view',
20
+ });
21
+ }