@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
@@ -3,27 +3,24 @@ import { FIXED_STEP } from '../../simulation/raceSimulation.js';
3
3
  import { clamp } from '../../simulation/simMath.js';
4
4
  import {
5
5
  domUpdateIntervalForSpeed,
6
- FRAME_PACING_EPSILON_MS,
7
6
  maxSimulationStepsForFrame,
8
7
  MAX_FRAME_CATCHUP_COUNT,
9
8
  TARGET_FRAME_MS,
10
9
  } from './runtimeTiming.js';
11
10
 
12
11
  export function runFrameLoopTick(app) {
13
- if (app.expertMode) return;
12
+ if (app.destroyed || app.expertMode) return;
13
+ const simulation = app.sim;
14
14
  const now = performance.now();
15
- if (now < app.nextGameFrameTime - FRAME_PACING_EPSILON_MS) return;
16
-
17
- const elapsedFrameCount = clamp(
18
- Math.floor((now - app.nextGameFrameTime + FRAME_PACING_EPSILON_MS) / TARGET_FRAME_MS) + 1,
19
- 1,
20
- MAX_FRAME_CATCHUP_COUNT,
21
- );
22
- const frameSeconds = (TARGET_FRAME_MS * elapsedFrameCount) / 1000;
23
- app.nextGameFrameTime += TARGET_FRAME_MS * elapsedFrameCount;
24
- if (now - app.nextGameFrameTime > TARGET_FRAME_MS * MAX_FRAME_CATCHUP_COUNT) {
25
- app.nextGameFrameTime = now + TARGET_FRAME_MS;
15
+ if (app.gridWalk?.tick(now)) {
16
+ app.lastTime = now;
17
+ return;
26
18
  }
19
+ // Pixi owns draw pacing. A second frame gate can draw stale poses, and
20
+ // rounding wall time to whole frames destroys the interpolation remainder.
21
+ const elapsedMs = clamp(now - app.lastTime, 0, TARGET_FRAME_MS * MAX_FRAME_CATCHUP_COUNT);
22
+ const elapsedFrameCount = elapsedMs / TARGET_FRAME_MS;
23
+ const frameSeconds = elapsedMs / 1000;
27
24
  app.lastTime = now;
28
25
  app.sampleFps(now);
29
26
  app.accumulator += frameSeconds * app.simulationSpeed;
@@ -31,23 +28,27 @@ export function runFrameLoopTick(app) {
31
28
  let simulationSteps = 0;
32
29
  const stepEvents = [];
33
30
  const maxSimulationSteps = maxSimulationStepsForFrame(app.simulationSpeed, elapsedFrameCount);
34
- while (app.accumulator >= FIXED_STEP && simulationSteps < maxSimulationSteps) {
31
+ while (app.accumulator >= FIXED_STEP - 1e-10 && simulationSteps < maxSimulationSteps) {
35
32
  app.sim.step(FIXED_STEP);
36
33
  if (Array.isArray(app.sim.events) && app.sim.events.length > 0) {
37
34
  stepEvents.push(...app.sim.events);
38
35
  }
39
- app.accumulator -= FIXED_STEP;
36
+ app.accumulator = Math.max(0, app.accumulator - FIXED_STEP);
40
37
  simulationSteps += 1;
41
38
  }
42
39
 
43
40
  if (app.accumulator >= FIXED_STEP) {
44
41
  app.accumulator %= FIXED_STEP;
45
- app.nextGameFrameTime = now + TARGET_FRAME_MS;
46
42
  }
47
43
 
48
44
  const shouldUpdateDom = now - app.lastDomUpdateTime >= domUpdateIntervalForSpeed(app.simulationSpeed);
49
45
  const fullSnapshot = stepEvents.length > 0 || shouldUpdateDom ? app.sim.snapshot() : null;
50
46
  if (stepEvents.length > 0) app.emitRaceEvents(stepEvents, fullSnapshot);
47
+ // Host lifecycle callbacks may synchronously unmount or restart the race.
48
+ // Do not render a disposed scene or deliver the old snapshot to its successor.
49
+ if (app.destroyed || app.sim !== simulation) return;
50
+ if (fullSnapshot) app.emitSnapshotLifecycle(fullSnapshot);
51
+ if (app.destroyed || app.sim !== simulation) return;
51
52
  const renderSource = fullSnapshot ??
52
53
  app.sim.snapshotRenderInto?.(app.renderSourceSnapshotBuffer ?? {}) ??
53
54
  app.sim.snapshotRender?.() ??
@@ -59,7 +60,7 @@ export function runFrameLoopTick(app) {
59
60
  clamp(app.accumulator / FIXED_STEP, 0, 1),
60
61
  );
61
62
  app.renderSnapshotBuffer = renderSnapshot;
62
- app.applyCamera(renderSnapshot);
63
+ app.applyCamera(renderSnapshot, { elapsedMs });
63
64
  app.renderDrsTrails(renderSnapshot);
64
65
  app.renderPitLaneStatus(renderSnapshot);
65
66
  app.renderCars(renderSnapshot);
@@ -0,0 +1,94 @@
1
+ import { ResizePlugin, TickerPlugin } from 'pixi.js';
2
+
3
+ export class RuntimeStartupCancelledError extends Error {
4
+ constructor() {
5
+ super('PaddockJS initialization was cancelled before completion.');
6
+ this.name = 'RuntimeStartupCancelledError';
7
+ }
8
+ }
9
+
10
+ export function assertRuntimeStartupActive(app) {
11
+ if (app.destroyed) throw new RuntimeStartupCancelledError();
12
+ }
13
+
14
+ function releaseInitializedPixiPlugins(application, onCleanupError) {
15
+ // Runtime init owns Pixi's private ticker and resize target. Init can fail
16
+ // between these plugins, so calling Application.destroy() is unsafe here.
17
+ if (application.ticker) {
18
+ try {
19
+ TickerPlugin.destroy.call(application);
20
+ } catch (error) {
21
+ onCleanupError(error);
22
+ }
23
+ }
24
+ if (typeof application._cancelResize === 'function') {
25
+ try {
26
+ ResizePlugin.destroy.call(application);
27
+ } catch (error) {
28
+ onCleanupError(error);
29
+ }
30
+ }
31
+ }
32
+
33
+ // Pixi's application plugins cannot be destroyed until their async init has settled.
34
+ export function createPixiStartupLifecycle(application, onCleanupError = () => {}) {
35
+ let initializing = false;
36
+ let initialized = false;
37
+ let releaseRequested = false;
38
+ let released = false;
39
+
40
+ function release() {
41
+ if (initializing || released) return;
42
+ released = true;
43
+ let rendererCleanupNeeded = true;
44
+ if (initialized) {
45
+ try {
46
+ application.destroy(true, { children: true, texture: false });
47
+ return;
48
+ } catch (error) {
49
+ // Pixi clears stage immediately before calling renderer.destroy().
50
+ // A renderer failure must be reported without invoking that disposer twice.
51
+ rendererCleanupNeeded = application.stage != null;
52
+ onCleanupError(error);
53
+ }
54
+ }
55
+ // Renderer discovery or an application plugin can fail before all plugins
56
+ // are installed. Release only the instance-owned plugins that reached init.
57
+ releaseInitializedPixiPlugins(application, onCleanupError);
58
+ try {
59
+ if (!application.stage?.destroyed) application.stage?.destroy?.({ children: true, texture: false });
60
+ } catch (error) {
61
+ onCleanupError(error);
62
+ } finally {
63
+ application.stage = null;
64
+ try {
65
+ if (rendererCleanupNeeded) application.renderer?.destroy?.(true);
66
+ } catch (error) {
67
+ onCleanupError(error);
68
+ } finally {
69
+ application.renderer = null;
70
+ }
71
+ }
72
+ }
73
+
74
+ return {
75
+ async initialize(options) {
76
+ initializing = true;
77
+ try {
78
+ await application.init(options);
79
+ initialized = true;
80
+ // Pixi defaults to owning touch gestures. Let the host page scroll;
81
+ // Grid Walk opts into dragging through its own scoped touch-action CSS.
82
+ if (application.renderer.events) application.renderer.events.autoPreventDefault = false;
83
+ application.canvas.style?.removeProperty('touch-action');
84
+ } finally {
85
+ initializing = false;
86
+ if (releaseRequested) release();
87
+ }
88
+ },
89
+ destroy() {
90
+ releaseRequested = true;
91
+ release();
92
+ },
93
+ };
94
+ }
@@ -2,13 +2,16 @@ import { clamp } from '../../simulation/simMath.js';
2
2
 
3
3
  export const TARGET_RENDER_FPS = 60;
4
4
  export const TARGET_FRAME_MS = 1000 / TARGET_RENDER_FPS;
5
- export const FRAME_PACING_EPSILON_MS = 0.75;
6
5
  export const MAX_FRAME_CATCHUP_COUNT = 4;
7
6
  export const BASE_MAX_SIMULATION_STEPS_PER_RENDER = 5;
8
7
  export const HARD_MAX_SIMULATION_STEPS_PER_RENDER = 60;
9
8
  export const DOM_UPDATE_INTERVAL_MS = 100;
10
9
  export const TIMING_UPDATE_INTERVAL_MS = 250;
11
10
 
11
+ export function configureRuntimeTicker(ticker) {
12
+ if (ticker) ticker.maxFPS = TARGET_RENDER_FPS;
13
+ }
14
+
12
15
  export function maxSimulationStepsForFrame(simulationSpeed, elapsedFrameCount) {
13
16
  const speed = Math.max(1, Number(simulationSpeed) || 1);
14
17
  const frameCount = Math.max(1, Number(elapsedFrameCount) || 1);
@@ -68,7 +68,8 @@ export function syncRuntimeTicker(app) {
68
68
  const ticker = app.app?.ticker;
69
69
  if (!ticker) return;
70
70
 
71
- const shouldRun = app.runtimeViewportVisible && app.runtimeDocumentVisible;
71
+ const shouldRun = app.runtimeViewportVisible && app.runtimeDocumentVisible &&
72
+ (!app.gridWalk?.active || app.gridWalk.needsAnimation);
72
73
  const tickerRunning = typeof ticker.started === 'boolean'
73
74
  ? ticker.started
74
75
  : app.runtimeTickerRunning;
@@ -76,7 +77,11 @@ export function syncRuntimeTicker(app) {
76
77
 
77
78
  app.runtimeTickerRunning = shouldRun;
78
79
  if (shouldRun) {
79
- app.resetFrameClock();
80
+ if (app.gridWalk) {
81
+ // Grid Walk pauses retain the fractional fixed step across ticker stops.
82
+ app.lastTime = performance.now();
83
+ app.gridWalk.lastTime = app.lastTime;
84
+ } else app.resetFrameClock();
80
85
  ticker.start?.();
81
86
  } else {
82
87
  ticker.stop?.();
@@ -1,4 +1,4 @@
1
- import { normalizeCssColorToken } from './themeOptions.js';
1
+ import { normalizeCssColorToken } from './theme/cssTokens.js';
2
2
 
3
3
  export const DEFAULT_SAFE_DRIVER_COLOR = '#e10600';
4
4
 
@@ -1,124 +1,17 @@
1
+ import { DEFAULT_F1_SIMULATOR_OPTIONS, PADDOCK_SIMULATOR_PRESETS } from './simulatorDefaults.js';
2
+ import { resolveSimulatorUiOptions } from './uiOptions.js';
3
+ export { DEFAULT_F1_SIMULATOR_OPTIONS, PADDOCK_SIMULATOR_PRESETS } from './simulatorDefaults.js';
1
4
  import { resolveF1SimulatorAssets } from './defaultAssets.js';
2
5
  import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../data/championship.js';
3
6
  import { normalizeSimulatorDrivers } from '../data/normalizeDrivers.js';
4
7
  import { normalizePhysicsMode } from '../simulation/vehicle/vehiclePhysics.js';
5
8
  import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
6
9
  import {
7
- DEFAULT_PADDOCK_THEME_INPUT,
8
- applyPaddockThemeCssVariables,
9
10
  mergeThemeInputs,
10
11
  normalizePaddockTheme,
11
12
  } from './themeOptions.js';
12
- import { normalizeTimingGapMode } from './timingGapMode.js';
13
13
  import { normalizePublicUrlOption } from './urlOptions.js';
14
14
 
15
- export const DEFAULT_F1_SIMULATOR_OPTIONS = {
16
- seed: 1971,
17
- physicsMode: 'arcade',
18
- warmup: {
19
- enabled: true,
20
- policy: 'config-change',
21
- },
22
- totalLaps: 10,
23
- initialCameraMode: 'leader',
24
- title: 'F1 Simulator Lab',
25
- kicker: 'Race Control',
26
- backLinkHref: 'projects.html',
27
- backLinkLabel: 'Projects',
28
- showBackLink: true,
29
- ui: {
30
- layoutPreset: 'standard',
31
- cameraControls: 'external',
32
- showFps: true,
33
- showTimingTower: true,
34
- showTelemetry: true,
35
- telemetryIncludesOverview: true,
36
- telemetryModules: {
37
- core: true,
38
- sectors: true,
39
- lapTimes: true,
40
- sectorTimes: true,
41
- },
42
- showRaceDataPanel: true,
43
- raceDataTelemetryDetail: false,
44
- driverCamera: false,
45
- raceDataBanners: {
46
- initial: 'project',
47
- enabled: ['project', 'radio'],
48
- },
49
- raceDataBannerSize: 'custom',
50
- timingTowerVerticalFit: 'expand-race-view',
51
- timingGapMode: 'interval',
52
- timingGapModeToggle: true,
53
- timingEntryVerticalPadding: 5,
54
- responsiveNarrowLayout: true,
55
- },
56
- debug: {
57
- physicsModeIndicator: false,
58
- },
59
- theme: DEFAULT_PADDOCK_THEME_INPUT,
60
- };
61
-
62
- export const PADDOCK_SIMULATOR_PRESETS = {
63
- dashboard: {
64
- ui: {
65
- layoutPreset: 'standard',
66
- cameraControls: 'external',
67
- showFps: true,
68
- showTimingTower: true,
69
- showTelemetry: true,
70
- showRaceDataPanel: true,
71
- raceDataBannerSize: 'custom',
72
- timingTowerVerticalFit: 'expand-race-view',
73
- },
74
- },
75
- 'timing-overlay': {
76
- ui: {
77
- layoutPreset: 'left-tower-overlay',
78
- cameraControls: 'external',
79
- showFps: false,
80
- showTimingTower: true,
81
- showTelemetry: true,
82
- showRaceDataPanel: true,
83
- raceDataBannerSize: 'auto',
84
- timingTowerVerticalFit: 'expand-race-view',
85
- raceDataBanners: {
86
- initial: 'project',
87
- enabled: ['project', 'radio'],
88
- },
89
- },
90
- },
91
- 'compact-race': {
92
- ui: {
93
- layoutPreset: 'standard',
94
- cameraControls: 'external',
95
- showFps: false,
96
- showTimingTower: false,
97
- showTelemetry: false,
98
- showRaceDataPanel: true,
99
- raceDataBannerSize: 'auto',
100
- timingTowerVerticalFit: 'scroll',
101
- },
102
- theme: {
103
- timingTowerMaxWidth: '340px',
104
- raceViewMinHeight: '460px',
105
- },
106
- },
107
- 'full-dashboard': {
108
- ui: {
109
- layoutPreset: 'standard',
110
- cameraControls: 'external',
111
- showFps: true,
112
- showTimingTower: true,
113
- showTelemetry: true,
114
- telemetryIncludesOverview: true,
115
- showRaceDataPanel: true,
116
- raceDataBannerSize: 'custom',
117
- timingTowerVerticalFit: 'expand-race-view',
118
- },
119
- },
120
- };
121
-
122
15
  export {
123
16
  DEFAULT_PADDOCK_THEME,
124
17
  PADDOCK_THEME_CSS_VARIABLES,
@@ -130,12 +23,10 @@ export {
130
23
 
131
24
  const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'driver', 'show-all', 'pit']);
132
25
 
133
- function normalizeNonNegativeFiniteNumber(value, fallback) {
134
- const numericValue = Number(value);
135
- return Number.isFinite(numericValue) && numericValue >= 0 ? numericValue : fallback;
136
- }
137
-
138
26
  export function resolveF1SimulatorOptions(options = {}) {
27
+ if (options.gridWalk?.enabled && options.expert?.enabled) {
28
+ throw new Error('Grid Walk requires normal browser playback; expert mode owns its own simulation lifecycle.');
29
+ }
139
30
  const presetName = Object.hasOwn(PADDOCK_SIMULATOR_PRESETS, options.preset)
140
31
  ? options.preset
141
32
  : null;
@@ -145,39 +36,7 @@ export function resolveF1SimulatorOptions(options = {}) {
145
36
  const initialCameraMode = SUPPORTED_CAMERA_MODES.has(options.initialCameraMode)
146
37
  ? options.initialCameraMode
147
38
  : DEFAULT_F1_SIMULATOR_OPTIONS.initialCameraMode;
148
- const ui = {
149
- ...DEFAULT_F1_SIMULATOR_OPTIONS.ui,
150
- ...presetUi,
151
- ...requestedUi,
152
- raceDataBanners: {
153
- ...DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners,
154
- ...(presetUi.raceDataBanners ?? {}),
155
- ...(requestedUi.raceDataBanners ?? {}),
156
- },
157
- };
158
- ui.driverCamera = Boolean(ui.driverCamera || initialCameraMode === 'driver');
159
- ui.raceDataBanners.enabled = normalizeEnabledBanners(ui.raceDataBanners.enabled);
160
- ui.telemetryModules = normalizeTelemetryModules(ui.telemetryModules);
161
- if (!['project', 'radio', 'hidden'].includes(ui.raceDataBanners.initial)) {
162
- ui.raceDataBanners.initial = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.initial;
163
- }
164
- if (ui.raceDataBanners.initial !== 'hidden' && !ui.raceDataBanners.enabled.includes(ui.raceDataBanners.initial)) {
165
- ui.raceDataBanners.initial = 'hidden';
166
- }
167
- if (!['expand-race-view', 'scroll'].includes(ui.timingTowerVerticalFit)) {
168
- ui.timingTowerVerticalFit = DEFAULT_F1_SIMULATOR_OPTIONS.ui.timingTowerVerticalFit;
169
- }
170
- ui.timingGapMode = normalizeTimingGapMode(ui.timingGapMode);
171
- ui.timingGapModeToggle = ui.timingGapModeToggle !== false;
172
- ui.timingEntryVerticalPadding = normalizeNonNegativeFiniteNumber(
173
- ui.timingEntryVerticalPadding,
174
- DEFAULT_F1_SIMULATOR_OPTIONS.ui.timingEntryVerticalPadding,
175
- );
176
- ui.responsiveNarrowLayout = ui.responsiveNarrowLayout !== false;
177
- if (!['auto', 'custom'].includes(ui.raceDataBannerSize)) {
178
- ui.raceDataBannerSize = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBannerSize;
179
- }
180
- ui.raceDataTelemetryDetail = Boolean(ui.raceDataTelemetryDetail);
39
+ const ui = resolveSimulatorUiOptions(requestedUi, presetUi, initialCameraMode);
181
40
  const debug = {
182
41
  ...DEFAULT_F1_SIMULATOR_OPTIONS.debug,
183
42
  ...(preset.debug ?? {}),
@@ -211,6 +70,11 @@ export function resolveF1SimulatorOptions(options = {}) {
211
70
  physicsMode: normalizePhysicsMode(options.physicsMode ?? preset.physicsMode),
212
71
  warmup: normalizeWarmupOptions(mergedOptions.warmup, 'browser'),
213
72
  initialCameraMode,
73
+ gridWalk: {
74
+ enabled: options.gridWalk?.enabled === true,
75
+ initialView: options.gridWalk?.initialView === 'race' ? 'race' : 'grid-walk',
76
+ ambientMotion: options.gridWalk?.ambientMotion !== false,
77
+ },
214
78
  ui,
215
79
  debug,
216
80
  theme,
@@ -230,28 +94,3 @@ function collectDriverTeamThemes(drivers = []) {
230
94
  });
231
95
  return Object.keys(teamThemes).length > 0 ? { teamThemes } : {};
232
96
  }
233
-
234
- function normalizeEnabledBanners(value) {
235
- if (value === false) return [];
236
- if (value === true || value == null) return [...DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.enabled];
237
- if (!Array.isArray(value)) return [...DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.enabled];
238
- return [...new Set(value.filter((item) => item === 'project' || item === 'radio'))];
239
- }
240
-
241
- function normalizeTelemetryModules(value) {
242
- const defaults = DEFAULT_F1_SIMULATOR_OPTIONS.ui.telemetryModules;
243
- const names = Object.keys(defaults);
244
- if (value === false) {
245
- return Object.fromEntries(names.map((name) => [name, false]));
246
- }
247
- if (value === true || value == null) return { ...defaults };
248
- if (Array.isArray(value)) {
249
- const requested = new Set(value);
250
- return Object.fromEntries(names.map((name) => [name, requested.has(name)]));
251
- }
252
- if (typeof value !== 'object') return { ...defaults };
253
- return Object.fromEntries(names.map((name) => [
254
- name,
255
- value[name] == null ? defaults[name] : Boolean(value[name]),
256
- ]));
257
- }
@@ -8,6 +8,7 @@ export function mergeRestartOptions(previousOptions, nextOptions = {}) {
8
8
  return {
9
9
  ...previousOptions,
10
10
  ...nextOptions,
11
+ gridWalk: { ...previousOptions.gridWalk, ...nextOptions.gridWalk },
11
12
  ui: {
12
13
  ...previousUi,
13
14
  ...(nextOptions.ui ?? {}),
@@ -0,0 +1,10 @@
1
+ export function createRuntimeSeed() {
2
+ const values = new Uint32Array(1);
3
+ try {
4
+ globalThis.crypto?.getRandomValues?.(values);
5
+ } catch {
6
+ values[0] = 0;
7
+ }
8
+ const highResolutionTime = globalThis.performance?.now?.() ?? 0;
9
+ return (values[0] || Math.floor(Date.now() + highResolutionTime * 1000)) >>> 0;
10
+ }
@@ -0,0 +1,106 @@
1
+ import { DEFAULT_PADDOCK_THEME_INPUT } from './theme/themeTokens.js';
2
+ import { DEFAULT_TELEMETRY_MODULES } from './telemetryModules.js';
3
+
4
+ export const DEFAULT_F1_SIMULATOR_OPTIONS = {
5
+ seed: 1971,
6
+ physicsMode: 'arcade',
7
+ warmup: {
8
+ enabled: true,
9
+ policy: 'config-change',
10
+ },
11
+ totalLaps: 10,
12
+ initialCameraMode: 'leader',
13
+ gridWalk: { enabled: false, initialView: 'grid-walk', ambientMotion: true },
14
+ title: 'F1 Simulator Lab',
15
+ kicker: 'Race Control',
16
+ backLinkHref: 'projects.html',
17
+ backLinkLabel: 'Projects',
18
+ showBackLink: true,
19
+ ui: {
20
+ layoutPreset: 'standard',
21
+ cameraControls: 'external',
22
+ viewControls: 'switch',
23
+ showFps: true,
24
+ showTimingTower: true,
25
+ showTelemetry: true,
26
+ telemetryIncludesOverview: true,
27
+ telemetryModules: { ...DEFAULT_TELEMETRY_MODULES },
28
+ showRaceDataPanel: true,
29
+ raceDataTelemetryDetail: false,
30
+ driverCamera: false,
31
+ raceDataBanners: {
32
+ initial: 'project',
33
+ enabled: ['project', 'radio'],
34
+ },
35
+ raceDataBannerSize: 'custom',
36
+ timingTowerVerticalFit: 'expand-race-view',
37
+ timingGapMode: 'interval',
38
+ timingGapModeToggle: true,
39
+ timingEntryVerticalPadding: 5,
40
+ responsiveNarrowLayout: true,
41
+ },
42
+ debug: {
43
+ physicsModeIndicator: false,
44
+ },
45
+ theme: DEFAULT_PADDOCK_THEME_INPUT,
46
+ };
47
+
48
+ export const PADDOCK_SIMULATOR_PRESETS = {
49
+ dashboard: {
50
+ ui: {
51
+ layoutPreset: 'standard',
52
+ cameraControls: 'external',
53
+ showFps: true,
54
+ showTimingTower: true,
55
+ showTelemetry: true,
56
+ showRaceDataPanel: true,
57
+ raceDataBannerSize: 'custom',
58
+ timingTowerVerticalFit: 'expand-race-view',
59
+ },
60
+ },
61
+ 'timing-overlay': {
62
+ ui: {
63
+ layoutPreset: 'left-tower-overlay',
64
+ cameraControls: 'external',
65
+ showFps: false,
66
+ showTimingTower: true,
67
+ showTelemetry: true,
68
+ showRaceDataPanel: true,
69
+ raceDataBannerSize: 'auto',
70
+ timingTowerVerticalFit: 'expand-race-view',
71
+ raceDataBanners: {
72
+ initial: 'project',
73
+ enabled: ['project', 'radio'],
74
+ },
75
+ },
76
+ },
77
+ 'compact-race': {
78
+ ui: {
79
+ layoutPreset: 'standard',
80
+ cameraControls: 'external',
81
+ showFps: false,
82
+ showTimingTower: false,
83
+ showTelemetry: false,
84
+ showRaceDataPanel: true,
85
+ raceDataBannerSize: 'auto',
86
+ timingTowerVerticalFit: 'scroll',
87
+ },
88
+ theme: {
89
+ timingTowerMaxWidth: '340px',
90
+ raceViewMinHeight: '460px',
91
+ },
92
+ },
93
+ 'full-dashboard': {
94
+ ui: {
95
+ layoutPreset: 'standard',
96
+ cameraControls: 'external',
97
+ showFps: true,
98
+ showTimingTower: true,
99
+ showTelemetry: true,
100
+ telemetryIncludesOverview: true,
101
+ showRaceDataPanel: true,
102
+ raceDataBannerSize: 'custom',
103
+ timingTowerVerticalFit: 'expand-race-view',
104
+ },
105
+ },
106
+ };
@@ -0,0 +1,20 @@
1
+ export const DEFAULT_TELEMETRY_MODULES = Object.freeze({
2
+ core: true,
3
+ sectors: true,
4
+ lapTimes: true,
5
+ sectorTimes: true,
6
+ });
7
+
8
+ export function normalizeTelemetryModules(value, defaults = DEFAULT_TELEMETRY_MODULES) {
9
+ const names = Object.keys(defaults);
10
+ if (value === false) return Object.fromEntries(names.map((name) => [name, false]));
11
+ if (value === true || value == null || typeof value !== 'object') return { ...defaults };
12
+ if (Array.isArray(value)) {
13
+ const requested = new Set(value);
14
+ return Object.fromEntries(names.map((name) => [name, requested.has(name)]));
15
+ }
16
+ return Object.fromEntries(names.map((name) => [
17
+ name,
18
+ value[name] == null ? defaults[name] : Boolean(value[name]),
19
+ ]));
20
+ }
@@ -0,0 +1,85 @@
1
+ const GENERATED_COLOR_CACHE = new Map();
2
+
3
+ export function normalizeCssColorToken(value, fallback = null) {
4
+ if (typeof value !== 'string') return fallback;
5
+ const candidate = value.trim();
6
+ if (
7
+ candidate === '' ||
8
+ /[\u0000-\u001F\u007F;]/.test(candidate) ||
9
+ /[{};]/.test(candidate) ||
10
+ /\b(?:url|expression)\s*\(/i.test(candidate)
11
+ ) return fallback;
12
+
13
+ if (/^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(candidate)) return normalizeHexColor(candidate);
14
+ if (/^(?:rgb|rgba|hsl|hsla)\([0-9\s.,%/+*-]+\)$/i.test(candidate)) return candidate;
15
+ if (/^(?:transparent|currentcolor|black|white|red|green|blue|yellow|orange|purple|pink|gray|grey)$/i.test(candidate)) {
16
+ return candidate.toLowerCase();
17
+ }
18
+ return fallback;
19
+ }
20
+
21
+ export function normalizeCssSizeToken(value, fallback = null) {
22
+ if (typeof value !== 'string' && typeof value !== 'number') return fallback;
23
+ const candidate = String(value).trim();
24
+ if (
25
+ candidate === '' ||
26
+ /[\u0000-\u001F\u007F;]/.test(candidate) ||
27
+ /[{};]/.test(candidate) ||
28
+ /\b(?:url|expression)\s*\(/i.test(candidate)
29
+ ) return fallback;
30
+ return candidate;
31
+ }
32
+
33
+ export function deriveModeColor(mode, key, value) {
34
+ const cacheKey = `${mode}:${key}:${value}`;
35
+ if (GENERATED_COLOR_CACHE.has(cacheKey)) return GENERATED_COLOR_CACHE.get(cacheKey);
36
+
37
+ const rgb = parseHexColor(value);
38
+ const generated = rgb ? rgbToHex(adjustRgbForMode(rgb, mode)) : value;
39
+ GENERATED_COLOR_CACHE.set(cacheKey, generated);
40
+ return generated;
41
+ }
42
+
43
+ function parseHexColor(value) {
44
+ const normalized = normalizeCssColorToken(value, null);
45
+ if (!normalized?.startsWith?.('#')) return null;
46
+ const hex = normalized.slice(1);
47
+ const expanded = hex.length === 3
48
+ ? hex.split('').map((char) => `${char}${char}`).join('')
49
+ : hex;
50
+ return {
51
+ r: Number.parseInt(expanded.slice(0, 2), 16),
52
+ g: Number.parseInt(expanded.slice(2, 4), 16),
53
+ b: Number.parseInt(expanded.slice(4, 6), 16),
54
+ };
55
+ }
56
+
57
+ function normalizeHexColor(value) {
58
+ const hex = value.slice(1);
59
+ if (hex.length === 3) {
60
+ return `#${hex.split('').map((char) => `${char}${char}`).join('')}`.toLowerCase();
61
+ }
62
+ return value.toLowerCase();
63
+ }
64
+
65
+ function adjustRgbForMode(rgb, mode) {
66
+ const amount = mode === 'light' ? 0.18 : -0.18;
67
+ return {
68
+ r: adjustChannel(rgb.r, amount),
69
+ g: adjustChannel(rgb.g, amount),
70
+ b: adjustChannel(rgb.b, amount),
71
+ };
72
+ }
73
+
74
+ function adjustChannel(channel, amount) {
75
+ const target = amount > 0 ? 255 : 0;
76
+ return Math.round(channel + (target - channel) * Math.abs(amount));
77
+ }
78
+
79
+ function rgbToHex({ r, g, b }) {
80
+ return `#${[r, g, b].map((channel) => clampColorChannel(channel).toString(16).padStart(2, '0')).join('')}`;
81
+ }
82
+
83
+ function clampColorChannel(value) {
84
+ return Math.max(0, Math.min(255, Math.round(value)));
85
+ }