@inventure71/paddockjs 4.1.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 (257) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/INSTALL_AND_UPDATE.md +31 -5
  3. package/README.md +34 -3
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +111 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +32 -16
  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/getting-started.md +3 -0
  16. package/docs/grid_walk.md +80 -0
  17. package/docs/index.md +5 -2
  18. package/docs/keyboard_driving_fix.md +118 -0
  19. package/docs/limit_driving_2026-10-14.md +88 -0
  20. package/docs/loading.md +141 -0
  21. package/docs/maintainability_audit_2026-09-08.md +146 -0
  22. package/docs/racing_ai_improvements.md +145 -0
  23. package/docs/rules.md +43 -13
  24. package/docs/sense_contract.md +4 -1
  25. package/docs/system_specs.md +40 -12
  26. package/docs/testing-reliability-guide.md +67 -0
  27. package/docs/v11_improvements.md +108 -0
  28. package/package.json +23 -15
  29. package/src/api/MountedSimulatorComponents.js +101 -0
  30. package/src/api/PaddockSimulatorController.js +56 -81
  31. package/src/app/BrowserExpertAdapter.js +0 -6
  32. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  33. package/src/app/ExternalTrackSnapshot.js +50 -0
  34. package/src/app/F1SimulatorApp.js +242 -297
  35. package/src/app/HostLifecycleEmitter.js +93 -0
  36. package/src/app/banners/raceDataBannerController.js +9 -4
  37. package/src/app/camera/cameraConstants.js +1 -1
  38. package/src/app/camera/cameraController.js +10 -13
  39. package/src/app/camera/cameraGeometry.js +1 -1
  40. package/src/app/controls/SimulatorViewControls.js +45 -0
  41. package/src/app/controls/workbenchBindings.js +64 -0
  42. package/src/app/controls/workbenchDisclosure.js +41 -0
  43. package/src/app/domBindings.js +21 -13
  44. package/src/app/gridWalk/GridWalkController.js +180 -0
  45. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  46. package/src/app/gridWalk/gridWalkControls.js +24 -0
  47. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  48. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  49. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  50. package/src/app/gridWalk/gridWalkNavigation.js +99 -0
  51. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  52. package/src/app/gridWalk/gridWalkSession.js +50 -0
  53. package/src/app/layoutSupport.js +2 -0
  54. package/src/app/raceOverlayClearanceSupport.js +21 -7
  55. package/src/app/readouts/carOverviewRenderer.js +9 -4
  56. package/src/app/readouts/readoutFormatters.js +1 -8
  57. package/src/app/readouts/telemetryRenderer.js +1 -3
  58. package/src/app/readouts/timingTowerRenderer.js +48 -50
  59. package/src/app/rendering/appAssets.js +8 -10
  60. package/src/app/rendering/carRenderer.js +27 -79
  61. package/src/app/rendering/displayUtils.js +0 -6
  62. package/src/app/rendering/expertSensorRenderer.js +1 -1
  63. package/src/app/rendering/replayGhostRenderer.js +4 -1
  64. package/src/app/rendering/trackRenderer.js +2 -2
  65. package/src/app/runtime/frameLoop.js +18 -17
  66. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  67. package/src/app/runtime/runtimeTiming.js +4 -1
  68. package/src/app/runtime/runtimeVisibility.js +7 -2
  69. package/src/config/cssValues.js +1 -1
  70. package/src/config/defaultOptions.js +12 -173
  71. package/src/config/restartOptions.js +1 -0
  72. package/src/config/runtimeSeed.js +10 -0
  73. package/src/config/simulatorDefaults.js +106 -0
  74. package/src/config/telemetryModules.js +20 -0
  75. package/src/config/theme/cssTokens.js +85 -0
  76. package/src/config/theme/themeCss.js +72 -0
  77. package/src/config/theme/themeInputs.js +155 -0
  78. package/src/config/theme/themeResolver.js +172 -0
  79. package/src/config/theme/themeSelectors.js +69 -0
  80. package/src/config/theme/themeTokens.js +257 -0
  81. package/src/config/theme/themeUtils.js +15 -0
  82. package/src/config/themeOptions.js +14 -830
  83. package/src/config/timingGapMode.js +0 -2
  84. package/src/config/uiOptions.js +53 -0
  85. package/src/data/championship.js +16 -158
  86. package/src/data/championshipDefaults.js +87 -0
  87. package/src/data/driverData.js +10 -20
  88. package/src/data/entryIdentity.js +67 -0
  89. package/src/data/entryPresentation.js +23 -0
  90. package/src/data/entryTypes.d.ts +160 -0
  91. package/src/data/index.d.ts +12 -195
  92. package/src/data/ratingScale.js +14 -0
  93. package/src/data/teamTypes.d.ts +23 -0
  94. package/src/data/trackTypes.d.ts +101 -0
  95. package/src/data/vehicleData.js +12 -23
  96. package/src/environment/actions.js +3 -2
  97. package/src/environment/controllerLoop.js +57 -44
  98. package/src/environment/controllerScheduler.js +30 -0
  99. package/src/environment/driverObservations.js +260 -0
  100. package/src/environment/driverReset.js +51 -0
  101. package/src/environment/episode.js +0 -1
  102. package/src/environment/evaluation.js +6 -90
  103. package/src/environment/evaluationTracker.js +100 -0
  104. package/src/environment/events.js +23 -0
  105. package/src/environment/index.d.ts +35 -91
  106. package/src/environment/metrics.js +3 -17
  107. package/src/environment/observationSchema.js +104 -0
  108. package/src/environment/observationVector.js +16 -115
  109. package/src/environment/observations.js +16 -316
  110. package/src/environment/options.js +5 -24
  111. package/src/environment/renderFrames.js +21 -0
  112. package/src/environment/resetOptions.js +41 -0
  113. package/src/environment/results.js +125 -0
  114. package/src/environment/rewards.js +10 -16
  115. package/src/environment/runtime.js +17 -300
  116. package/src/environment/scenarioOptions.js +90 -0
  117. package/src/environment/scenarioPlacements.js +70 -0
  118. package/src/environment/scenarioPresets.js +78 -0
  119. package/src/environment/scenarios.js +5 -236
  120. package/src/environment/scratch.js +18 -0
  121. package/src/environment/sensorOptions.js +20 -0
  122. package/src/environment/sensors/index.js +2 -556
  123. package/src/environment/sensors/indexedRayBands.js +0 -18
  124. package/src/environment/sensors/nearbyCars.js +18 -15
  125. package/src/environment/sensors/opponentRadar.js +3 -2
  126. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  127. package/src/environment/sensors/rayBandTrace.js +1 -37
  128. package/src/environment/sensors/rayBatch.js +81 -0
  129. package/src/environment/sensors/rayConfig.js +26 -0
  130. package/src/environment/sensors/rayGeometry.js +0 -8
  131. package/src/environment/sensors/rayGuards.js +0 -6
  132. package/src/environment/sensors/rayOutputs.js +75 -0
  133. package/src/environment/sensors/rayQuery.js +110 -0
  134. package/src/environment/sensors/rayScratch.js +146 -0
  135. package/src/environment/sensors/sensorTargets.js +12 -4
  136. package/src/environment/sensors/surfaceRays.js +6 -61
  137. package/src/environment/sensors/trackRays.js +7 -80
  138. package/src/environment/simulationSetup.js +30 -0
  139. package/src/environment/specs.js +15 -109
  140. package/src/index.d.ts +43 -202
  141. package/src/index.js +28 -6
  142. package/src/placeholder/index.js +1 -8
  143. package/src/rendering/displayLifecycle.js +6 -0
  144. package/src/rendering/proceduralTrackAsset.js +1 -1
  145. package/src/rendering/renderSnapshot.js +3 -0
  146. package/src/rendering/track/finishLineRenderer.js +1 -1
  147. package/src/rendering/track/gridRenderer.js +1 -1
  148. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  149. package/src/rendering/track/pitLaneRenderer.js +1 -1
  150. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  151. package/src/rendering/track/trackRenderConstants.js +0 -1
  152. package/src/rendering/track/trackRenderGeometry.js +1 -7
  153. package/src/simulation/collisionGeometry.js +12 -20
  154. package/src/simulation/driver/advancedPathControls.js +53 -0
  155. package/src/simulation/driver/advancedRacePace.js +112 -0
  156. package/src/simulation/driver/advancedRacingControls.js +61 -0
  157. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  158. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  159. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  160. package/src/simulation/driver/arcadeRacePace.js +83 -0
  161. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  162. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  163. package/src/simulation/driver/driverControlConstants.js +0 -2
  164. package/src/simulation/driver/driverController.js +16 -5
  165. package/src/simulation/driver/edgeRecovery.js +2 -3
  166. package/src/simulation/driver/racingControls.js +10 -396
  167. package/src/simulation/driver/racingDriverProfile.js +48 -0
  168. package/src/simulation/driver/racingLinePlan.js +54 -20
  169. package/src/simulation/driver/rejoinControls.js +59 -112
  170. package/src/simulation/driver/safetyCarControls.js +12 -0
  171. package/src/simulation/driver/trafficScan.js +136 -27
  172. package/src/simulation/participants/participantInteractions.js +24 -15
  173. package/src/simulation/pit/pitCarPlacement.js +29 -0
  174. package/src/simulation/pit/pitFlow.js +8 -30
  175. package/src/simulation/pit/pitIntent.js +4 -3
  176. package/src/simulation/pit/pitOccupancy.js +2 -2
  177. package/src/simulation/pit/pitQueue.js +2 -18
  178. package/src/simulation/pit/pitRouteMovement.js +1 -36
  179. package/src/simulation/pit/pitRouting.js +5 -81
  180. package/src/simulation/pit/pitService.js +3 -32
  181. package/src/simulation/pit/pitServiceConstants.js +1 -0
  182. package/src/simulation/pit/pitServiceProgress.js +34 -0
  183. package/src/simulation/pit/pitSnapshots.js +8 -4
  184. package/src/simulation/pit/pitState.js +1 -5
  185. package/src/simulation/race/classification.js +1 -8
  186. package/src/simulation/race/raceFinish.js +3 -8
  187. package/src/simulation/race/raceOrder.js +2 -13
  188. package/src/simulation/race/raceProgress.js +6 -20
  189. package/src/simulation/race/raceSetup.js +1 -1
  190. package/src/simulation/race/raceStep.js +20 -11
  191. package/src/simulation/race/redFlag.js +2 -4
  192. package/src/simulation/race/safetyCar.js +5 -9
  193. package/src/simulation/race/stalledDnf.js +2 -4
  194. package/src/simulation/raceSimulation.js +6 -38
  195. package/src/simulation/replay/replayGhosts.js +44 -32
  196. package/src/simulation/rules/collisionSteward.js +0 -14
  197. package/src/simulation/rules/penaltyActions.js +37 -0
  198. package/src/simulation/rules/penaltyLedger.js +8 -1
  199. package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
  200. package/src/simulation/rules/rulesReview.js +20 -16
  201. package/src/simulation/rules/trackLimitsSteward.js +2 -2
  202. package/src/simulation/simMath.js +4 -0
  203. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  204. package/src/simulation/timing/drsState.js +8 -0
  205. package/src/simulation/timing/drsTiming.js +2 -4
  206. package/src/simulation/timing/lapTelemetry.js +7 -4
  207. package/src/simulation/timing/sectorPerformance.js +14 -6
  208. package/src/simulation/timing/timingLines.js +12 -6
  209. package/src/simulation/track/pitLaneAccess.js +2 -2
  210. package/src/simulation/track/pitLaneLayout.js +4 -135
  211. package/src/simulation/track/pitLaneModel.js +70 -0
  212. package/src/simulation/track/pitLaneState.js +0 -1
  213. package/src/simulation/track/pitServiceLayout.js +121 -0
  214. package/src/simulation/track/proceduralTrack.js +77 -0
  215. package/src/simulation/track/spatialQueries.js +76 -31
  216. package/src/simulation/track/trackBuilder.js +54 -0
  217. package/src/simulation/track/trackConstants.js +0 -22
  218. package/src/simulation/track/trackMath.js +4 -29
  219. package/src/simulation/track/trackModel.js +12 -240
  220. package/src/simulation/track/trackModelCache.js +105 -0
  221. package/src/simulation/track/trackNearestQueries.js +1218 -0
  222. package/src/simulation/track/trackQueryConstants.js +3 -0
  223. package/src/simulation/track/trackQueryConstruction.js +368 -0
  224. package/src/simulation/track/trackQueryIndex.js +12 -1933
  225. package/src/simulation/track/trackQuerySegments.js +174 -0
  226. package/src/simulation/track/trackQueryStats.js +12 -0
  227. package/src/simulation/track/trackRayQueries.js +165 -0
  228. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  229. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  230. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  231. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  232. package/src/simulation/vehicle/contactResolution.js +23 -9
  233. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  234. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  235. package/src/simulation/vehicle/runoffResponse.js +2 -4
  236. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  237. package/src/simulation/vehicle/vehicleGeometry.js +85 -103
  238. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  239. package/src/simulation/vehicle/vehiclePhysics.js +42 -196
  240. package/src/simulation/vehicle/vehicleSnapshots.js +18 -6
  241. package/src/simulation/vehicle/vehicleState.js +5 -3
  242. package/src/simulation/vehicle/wheelSurface.js +13 -432
  243. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  244. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  245. package/src/styles.css +173 -19
  246. package/src/ui/bannerTemplates.js +4 -2
  247. package/src/ui/cameraControlsTemplate.js +4 -2
  248. package/src/ui/gridWalkTemplate.js +29 -0
  249. package/src/ui/htmlEscaping.js +8 -0
  250. package/src/ui/raceCanvasTemplate.js +3 -2
  251. package/src/ui/shellTemplate.js +3 -0
  252. package/src/ui/telemetryDrawerTemplate.js +4 -2
  253. package/src/ui/telemetryTemplates.js +3 -22
  254. package/src/ui/templateUtils.js +2 -8
  255. package/src/ui/viewControlsTemplate.js +21 -0
  256. package/examples/train-basic-policy.mjs +0 -276
  257. package/examples/trainingData.mjs +0 -133
@@ -25,7 +25,7 @@ function writeLapTelemetryPosition(target, track, raceDistance, totalLaps = Infi
25
25
  const sectorIndex = Math.min(TELEMETRY_SECTOR_COUNT - 1, Math.floor(lapProgress / sectorLength));
26
26
  const sectorStart = sectorIndex * sectorLength;
27
27
 
28
- target.completedLaps = Math.floor(positiveDistance / track.length);
28
+ target.completedLaps = Math.min(Math.floor(positiveDistance / track.length), totalLaps);
29
29
  target.currentLap = Math.max(1, lapIndex + 1);
30
30
  target.currentSector = sectorIndex + 1;
31
31
  target.currentSectorProgress = clamp((lapProgress - sectorStart) / sectorLength, 0, 1);
@@ -143,7 +143,10 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
143
143
  telemetry.lastSectors = copySectorTimes(telemetry.lastSectors, telemetry.currentSectors);
144
144
  clearSectorTimes(telemetry.currentSectors);
145
145
  telemetry.currentLapStartedAt = crossingTime;
146
- telemetry.completedLaps = Math.max(telemetry.completedLaps + 1, Math.min(Math.floor(boundaryDistance / track.length), totalLaps));
146
+ telemetry.completedLaps = Math.min(
147
+ totalLaps,
148
+ Math.max(telemetry.completedLaps + 1, Math.floor(boundaryDistance / track.length)),
149
+ );
147
150
  }
148
151
 
149
152
  telemetry.currentSectorStartedAt = crossingTime;
@@ -207,7 +210,7 @@ function syncInProgressLapTelemetry(telemetry, currentTime, currentRaceDistance,
207
210
  const sectorProgress = ensureSectorTimesArray(telemetry.sectorProgress);
208
211
  const liveSectors = ensureSectorTimesArray(telemetry.liveSectors);
209
212
 
210
- telemetry.completedLaps = Math.floor(positiveDistance / track.length);
213
+ telemetry.completedLaps = Math.min(Math.floor(positiveDistance / track.length), totalLaps);
211
214
  telemetry.currentLap = Math.max(1, lapIndex + 1);
212
215
  telemetry.currentSector = activeIndex + 1;
213
216
  telemetry.currentSectorProgress = clamp((lapProgress - sectorStart) / sectorLength, 0, 1);
@@ -234,7 +237,7 @@ function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, t
234
237
  preserveCompletedSectors,
235
238
  });
236
239
  telemetry.sectorProgress = sectorProgress;
237
- telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(telemetry.completedLaps, totalLaps));
240
+ telemetry.completedLaps = Math.min(telemetry.completedLaps, totalLaps);
238
241
  telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
239
242
  telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
240
243
  syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors });
@@ -1,3 +1,6 @@
1
+ import { finiteOrNull } from '../simMath.js';
2
+ export { finiteOrNull } from '../simMath.js';
3
+
1
4
  import { TELEMETRY_SECTOR_COUNT } from './timingConstants.js';
2
5
 
3
6
  const OVERALL_BEST_0 = '_sectorPerformanceOverallBest0';
@@ -5,7 +8,7 @@ const OVERALL_BEST_1 = '_sectorPerformanceOverallBest1';
5
8
  const OVERALL_BEST_2 = '_sectorPerformanceOverallBest2';
6
9
 
7
10
  export function createEmptySectorTimes() {
8
- return Array.from({ length: TELEMETRY_SECTOR_COUNT }, () => null);
11
+ return new Array(TELEMETRY_SECTOR_COUNT).fill(null);
9
12
  }
10
13
 
11
14
  export function createEmptySectorPerformance() {
@@ -17,11 +20,19 @@ export function createEmptySectorPerformance() {
17
20
  }
18
21
 
19
22
  export function serializeSectorTimes(values) {
20
- return createEmptySectorTimes().map((_, index) => finiteOrNull(values?.[index]));
23
+ const result = new Array(TELEMETRY_SECTOR_COUNT);
24
+ for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
25
+ result[index] = finiteOrNull(values?.[index]);
26
+ }
27
+ return result;
21
28
  }
22
29
 
23
30
  export function serializeSectorPerformance(values) {
24
- return createEmptySectorTimes().map((_, index) => values?.[index] ?? null);
31
+ const result = new Array(TELEMETRY_SECTOR_COUNT);
32
+ for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
33
+ result[index] = values?.[index] ?? null;
34
+ }
35
+ return result;
25
36
  }
26
37
 
27
38
  export function updateSectorPerformance(cars, stats = null) {
@@ -100,9 +111,6 @@ export function updateSectorPerformance(cars, stats = null) {
100
111
  return true;
101
112
  }
102
113
 
103
- export function finiteOrNull(value) {
104
- return Number.isFinite(value) ? value : null;
105
- }
106
114
 
107
115
  function classifySectorPerformance(value, personalBest, overallBest) {
108
116
  if (!Number.isFinite(value)) return null;
@@ -76,7 +76,13 @@ export function createTimingLines(track) {
76
76
  export function getTimingLineNumber(track, raceDistance) {
77
77
  const spacing = track.timingLines?.spacing;
78
78
  if (!Number.isFinite(spacing) || spacing <= 0 || !Number.isFinite(raceDistance)) return null;
79
- return Math.floor(Math.max(0, raceDistance) / spacing);
79
+ const distance = Math.max(0, raceDistance);
80
+ let lineNumber = Math.floor(distance / spacing);
81
+ // Division can round onto a line whose stored product is still ahead of
82
+ // the car. Use the same representable boundary for lookup and recording.
83
+ if (lineNumber > 0 && lineNumber * spacing > distance) lineNumber -= 1;
84
+ else if ((lineNumber + 1) * spacing <= distance) lineNumber += 1;
85
+ return lineNumber;
80
86
  }
81
87
 
82
88
  export function resetTimingLineCrossings(car, currentTime) {
@@ -105,7 +111,7 @@ export function recordTimingLineCrossings(car, previousRaceDistance, currentTime
105
111
  ? latestLine - (track.timingLines.count * TIMING_LINE_HISTORY_LAPS)
106
112
  : null;
107
113
 
108
- if (Math.abs(travelled) <= 1e-6) {
114
+ if (travelled === 0) {
109
115
  car.timingLineLastUpdatedAt = currentTime;
110
116
  if (!Number.isFinite(cutoff) || cutoff <= 0 || cutoff === car.timingLinePruneCutoff) return;
111
117
  pruneTimingLineCrossings(car, cutoff);
@@ -113,12 +119,12 @@ export function recordTimingLineCrossings(car, previousRaceDistance, currentTime
113
119
  return;
114
120
  }
115
121
 
116
- if (travelled > 1e-6 && travelled < track.length / 2) {
117
- const firstLine = Math.floor(Math.max(0, previousDistance) / spacing) + 1;
118
- const lastLine = Math.floor(Math.max(0, currentDistance) / spacing);
122
+ if (travelled > 0 && travelled < track.length / 2) {
123
+ const firstLine = getTimingLineNumber(track, previousDistance) + 1;
124
+ const lastLine = getTimingLineNumber(track, currentDistance);
119
125
  for (let lineNumber = firstLine; lineNumber <= lastLine; lineNumber += 1) {
120
126
  const lineDistance = lineNumber * spacing;
121
- if (lineDistance <= previousDistance + 1e-6 || lineDistance > currentDistance + 1e-6) continue;
127
+ if (lineDistance <= previousDistance || lineDistance > currentDistance) continue;
122
128
  const ratio = clamp((lineDistance - previousDistance) / travelled, 0, 1);
123
129
  setTimingLineCrossing(car, lineNumber, previousTime + ratio * elapsed);
124
130
  }
@@ -1,5 +1,5 @@
1
- import { clamp, wrapDistance } from '../simMath.js';
2
- import { PIT_ACCESS_MAX_LENGTH, PIT_ACCESS_SAMPLE_STEPS, PIT_ACCESS_SEARCH_STEP, PIT_ACCESS_TRACK_OVERLAP, PIT_LANE_WIDTH } from './trackConstants.js';
1
+ import { wrapDistance } from '../simMath.js';
2
+ import { PIT_ACCESS_SAMPLE_STEPS, PIT_ACCESS_SEARCH_STEP, PIT_ACCESS_TRACK_OVERLAP } from './trackConstants.js';
3
3
  import { angleBetweenVectors, headingVector, normalizeVector, sampleCubicBezier, signedLateralOffsetToPoint } from './trackMath.js';
4
4
  import { offsetTrackPoint, pointAt } from './spatialQueries.js';
5
5
 
@@ -1,9 +1,12 @@
1
1
  import { clamp } from '../simMath.js';
2
2
  import { metersToSimUnits } from '../units.js';
3
- import { WORLD, PIT_BOX_COUNT, PIT_BOX_DEPTH, PIT_BOX_LENGTH, PIT_BOX_PAIR_GAP, PIT_BOX_TO_LANE_GAP, PIT_ENTRY_SEARCH_AFTER, PIT_ENTRY_SEARCH_BEFORE, PIT_EXIT_SEARCH_AFTER, PIT_EXIT_SEARCH_BEFORE, PIT_LANE_EDGE_GAP, PIT_LANE_ENTRY_BUFFER, PIT_LANE_EXIT_BUFFER, PIT_LANE_FINISH_RATIO, PIT_LANE_OFFSET_SEARCH_STEP, PIT_LANE_WIDTH, PIT_SERVICE_AREA_DEPTH, PIT_SERVICE_AREA_LENGTH, PIT_SERVICE_QUEUE_GAP, PIT_TEAM_COUNT, PIT_TEAM_GAP, PIT_BOXES_PER_TEAM, PIT_WORKING_LANE_GAP, PIT_WORKING_LANE_WIDTH, PIT_ACCESS_MAX_LENGTH, PIT_ACCESS_MIN_LENGTH, PIT_ACCESS_TANGENT_RATIO, PIT_TRACK_CLEARANCE_MARGIN } from './trackConstants.js';
3
+ import { WORLD, PIT_BOX_COUNT, PIT_BOX_DEPTH, PIT_BOX_TO_LANE_GAP, PIT_ENTRY_SEARCH_AFTER, PIT_ENTRY_SEARCH_BEFORE, PIT_EXIT_SEARCH_AFTER, PIT_EXIT_SEARCH_BEFORE, PIT_LANE_EDGE_GAP, PIT_LANE_ENTRY_BUFFER, PIT_LANE_EXIT_BUFFER, PIT_LANE_FINISH_RATIO, PIT_LANE_OFFSET_SEARCH_STEP, PIT_LANE_WIDTH, PIT_SERVICE_AREA_DEPTH, PIT_TEAM_COUNT, PIT_BOXES_PER_TEAM, PIT_WORKING_LANE_GAP, PIT_WORKING_LANE_WIDTH, PIT_ACCESS_MAX_LENGTH, PIT_ACCESS_MIN_LENGTH, PIT_ACCESS_TANGENT_RATIO, PIT_TRACK_CLEARANCE_MARGIN } from './trackConstants.js';
4
4
  import { clonePoint, distance, headingVector, interpolatePitPoint, nearestPointOnPolyline, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
5
5
  import { findPitAccessConnection, createPitAccessRoadCenterline } from './pitLaneAccess.js';
6
6
  import { pointAt } from './spatialQueries.js';
7
+ import { createPitBoxes, getPitBoxRunLength } from './pitServiceLayout.js';
8
+
9
+ export { getPitBoxRunLength, createPitBoxCorners, createPitRectangleCorners, createPitBoxes } from './pitServiceLayout.js';
7
10
 
8
11
  export function createStartStraightBasis(track) {
9
12
  const finish = pointAt(track, 0);
@@ -20,20 +23,6 @@ export function createStartStraightBasis(track) {
20
23
  };
21
24
  }
22
25
 
23
- export function getPitBoxRunLength({
24
- teamCount = PIT_TEAM_COUNT,
25
- boxesPerTeam = PIT_BOXES_PER_TEAM,
26
- boxLength = PIT_BOX_LENGTH,
27
- pairGap = PIT_BOX_PAIR_GAP,
28
- teamGap = PIT_TEAM_GAP,
29
- } = {}) {
30
- const boxCount = teamCount * boxesPerTeam;
31
- return (
32
- boxCount * boxLength +
33
- teamCount * Math.max(0, boxesPerTeam - 1) * pairGap +
34
- Math.max(0, teamCount - 1) * teamGap
35
- );
36
- }
37
26
 
38
27
  export function createPitLaneLayout() {
39
28
  const runLength = getPitBoxRunLength();
@@ -130,128 +119,8 @@ export function choosePitLanePlacement(track, baseLaneOffset, layout) {
130
119
  return candidates.sort((left, right) => right.score - left.score)[0];
131
120
  }
132
121
 
133
- export function createPitBoxCorners(center, forward, serviceNormal) {
134
- const halfLength = PIT_BOX_LENGTH / 2;
135
- const halfDepth = PIT_BOX_DEPTH / 2;
136
- return [
137
- {
138
- x: center.x + forward.x * halfLength + serviceNormal.x * halfDepth,
139
- y: center.y + forward.y * halfLength + serviceNormal.y * halfDepth,
140
- },
141
- {
142
- x: center.x - forward.x * halfLength + serviceNormal.x * halfDepth,
143
- y: center.y - forward.y * halfLength + serviceNormal.y * halfDepth,
144
- },
145
- {
146
- x: center.x - forward.x * halfLength - serviceNormal.x * halfDepth,
147
- y: center.y - forward.y * halfLength - serviceNormal.y * halfDepth,
148
- },
149
- {
150
- x: center.x + forward.x * halfLength - serviceNormal.x * halfDepth,
151
- y: center.y + forward.y * halfLength - serviceNormal.y * halfDepth,
152
- },
153
- ];
154
- }
155
122
 
156
- export function createPitRectangleCorners(center, forward, serviceNormal, length, depth) {
157
- const halfLength = length / 2;
158
- const halfDepth = depth / 2;
159
- return [
160
- {
161
- x: center.x + forward.x * halfLength + serviceNormal.x * halfDepth,
162
- y: center.y + forward.y * halfLength + serviceNormal.y * halfDepth,
163
- },
164
- {
165
- x: center.x - forward.x * halfLength + serviceNormal.x * halfDepth,
166
- y: center.y - forward.y * halfLength + serviceNormal.y * halfDepth,
167
- },
168
- {
169
- x: center.x - forward.x * halfLength - serviceNormal.x * halfDepth,
170
- y: center.y - forward.y * halfLength - serviceNormal.y * halfDepth,
171
- },
172
- {
173
- x: center.x + forward.x * halfLength - serviceNormal.x * halfDepth,
174
- y: center.y + forward.y * halfLength - serviceNormal.y * halfDepth,
175
- },
176
- ];
177
- }
178
-
179
- export function createPitBoxes({ laneStart, laneForward, laneLength, serviceNormal }) {
180
- const runLength = getPitBoxRunLength();
181
- let cursor = Math.max(PIT_SERVICE_QUEUE_GAP + PIT_BOX_LENGTH / 2, (laneLength - runLength) / 2);
182
- const workingLaneOffset = PIT_LANE_WIDTH / 2 + PIT_WORKING_LANE_GAP + PIT_WORKING_LANE_WIDTH / 2;
183
- const boxLateral = workingLaneOffset + PIT_WORKING_LANE_WIDTH / 2 + PIT_BOX_TO_LANE_GAP + PIT_BOX_DEPTH / 2;
184
- const boxes = [];
185
- const serviceAreas = [];
186
-
187
- for (let index = 0; index < PIT_BOX_COUNT; index += 1) {
188
- const distanceAlongLane = cursor + PIT_BOX_LENGTH / 2;
189
- const laneTarget = interpolatePitPoint(laneStart, laneForward, distanceAlongLane);
190
- const center = {
191
- x: laneTarget.x + serviceNormal.x * boxLateral,
192
- y: laneTarget.y + serviceNormal.y * boxLateral,
193
- };
194
- const teamIndex = Math.floor(index / PIT_BOXES_PER_TEAM);
195
- const teamBoxIndex = index % PIT_BOXES_PER_TEAM;
196
123
 
197
- boxes.push({
198
- id: `team-${teamIndex + 1}-box-${teamBoxIndex + 1}`,
199
- index,
200
- teamIndex,
201
- teamBoxIndex,
202
- distanceAlongLane,
203
- laneTarget,
204
- center,
205
- length: PIT_BOX_LENGTH,
206
- depth: PIT_BOX_DEPTH,
207
- corners: createPitBoxCorners(center, laneForward, serviceNormal),
208
- });
209
-
210
- if (teamBoxIndex === PIT_BOXES_PER_TEAM - 1) {
211
- const firstTeamBox = boxes[index - (PIT_BOXES_PER_TEAM - 1)];
212
- const serviceDistance = (firstTeamBox.distanceAlongLane + distanceAlongLane) / 2;
213
- const queueDistance = Math.max(PIT_SERVICE_QUEUE_GAP / 2, serviceDistance - PIT_SERVICE_QUEUE_GAP);
214
- const serviceCenter = projectPitPoint(
215
- laneStart,
216
- laneForward,
217
- serviceNormal,
218
- serviceDistance,
219
- workingLaneOffset,
220
- );
221
- const queuePoint = projectPitPoint(
222
- laneStart,
223
- laneForward,
224
- serviceNormal,
225
- queueDistance,
226
- workingLaneOffset,
227
- );
228
-
229
- serviceAreas.push({
230
- id: `team-${teamIndex + 1}-service`,
231
- index: teamIndex,
232
- teamIndex,
233
- distanceAlongLane: serviceDistance,
234
- queueDistanceAlongLane: queueDistance,
235
- laneTarget: serviceCenter,
236
- center: serviceCenter,
237
- queuePoint,
238
- length: PIT_SERVICE_AREA_LENGTH,
239
- depth: PIT_SERVICE_AREA_DEPTH,
240
- corners: createPitRectangleCorners(serviceCenter, laneForward, serviceNormal, PIT_SERVICE_AREA_LENGTH, PIT_SERVICE_AREA_DEPTH),
241
- queueCorners: createPitRectangleCorners(queuePoint, laneForward, serviceNormal, PIT_SERVICE_AREA_LENGTH * 0.72, PIT_SERVICE_AREA_DEPTH),
242
- garageBoxIds: boxes
243
- .slice(index - (PIT_BOXES_PER_TEAM - 1), index + 1)
244
- .map((box) => box.id),
245
- });
246
- }
247
-
248
- cursor += PIT_BOX_LENGTH;
249
- if (teamBoxIndex === 0) cursor += PIT_BOX_PAIR_GAP;
250
- else if (teamIndex < PIT_TEAM_COUNT - 1) cursor += PIT_TEAM_GAP;
251
- }
252
-
253
- return { boxes, serviceAreas, workingLaneOffset };
254
- }
255
124
 
256
125
  export function createPitLaneBounds(pitLane) {
257
126
  const points = [
@@ -0,0 +1,70 @@
1
+ function clonePointLike(point) {
2
+ if (!point) return point;
3
+ return { ...point };
4
+ }
5
+
6
+ function clonePointArray(points) {
7
+ return Array.isArray(points) ? points.map(clonePointLike) : points;
8
+ }
9
+
10
+ export function clonePitLaneModel(pitLane) {
11
+ if (!pitLane) return pitLane;
12
+ return {
13
+ ...pitLane,
14
+ entry: pitLane.entry ? {
15
+ ...pitLane.entry,
16
+ trackPoint: clonePointLike(pitLane.entry.trackPoint),
17
+ edgePoint: clonePointLike(pitLane.entry.edgePoint),
18
+ trackConnectPoint: clonePointLike(pitLane.entry.trackConnectPoint),
19
+ lanePoint: clonePointLike(pitLane.entry.lanePoint),
20
+ roadCenterline: clonePointArray(pitLane.entry.roadCenterline),
21
+ connector: clonePointArray(pitLane.entry.connector),
22
+ } : pitLane.entry,
23
+ exit: pitLane.exit ? {
24
+ ...pitLane.exit,
25
+ trackPoint: clonePointLike(pitLane.exit.trackPoint),
26
+ edgePoint: clonePointLike(pitLane.exit.edgePoint),
27
+ trackConnectPoint: clonePointLike(pitLane.exit.trackConnectPoint),
28
+ lanePoint: clonePointLike(pitLane.exit.lanePoint),
29
+ roadCenterline: clonePointArray(pitLane.exit.roadCenterline),
30
+ connector: clonePointArray(pitLane.exit.connector),
31
+ } : pitLane.exit,
32
+ mainLane: pitLane.mainLane ? {
33
+ ...pitLane.mainLane,
34
+ start: clonePointLike(pitLane.mainLane.start),
35
+ end: clonePointLike(pitLane.mainLane.end),
36
+ points: clonePointArray(pitLane.mainLane.points),
37
+ } : pitLane.mainLane,
38
+ workingLane: pitLane.workingLane ? {
39
+ ...pitLane.workingLane,
40
+ start: clonePointLike(pitLane.workingLane.start),
41
+ end: clonePointLike(pitLane.workingLane.end),
42
+ points: clonePointArray(pitLane.workingLane.points),
43
+ } : pitLane.workingLane,
44
+ fastLane: pitLane.fastLane ? { ...pitLane.fastLane } : pitLane.fastLane,
45
+ serviceNormal: clonePointLike(pitLane.serviceNormal),
46
+ boxes: Array.isArray(pitLane.boxes)
47
+ ? pitLane.boxes.map((box) => ({
48
+ ...box,
49
+ laneTarget: clonePointLike(box.laneTarget),
50
+ center: clonePointLike(box.center),
51
+ corners: clonePointArray(box.corners),
52
+ }))
53
+ : pitLane.boxes,
54
+ serviceAreas: Array.isArray(pitLane.serviceAreas)
55
+ ? pitLane.serviceAreas.map((area) => ({
56
+ ...area,
57
+ laneTarget: clonePointLike(area.laneTarget),
58
+ center: clonePointLike(area.center),
59
+ queuePoint: clonePointLike(area.queuePoint),
60
+ corners: clonePointArray(area.corners),
61
+ queueCorners: clonePointArray(area.queueCorners),
62
+ garageBoxIds: [...(area.garageBoxIds ?? [])],
63
+ pitCrew: area.pitCrew ? { ...area.pitCrew } : area.pitCrew,
64
+ }))
65
+ : pitLane.serviceAreas,
66
+ teams: Array.isArray(pitLane.teams)
67
+ ? pitLane.teams.map((team) => ({ ...team, boxIds: [...(team.boxIds ?? [])], pitCrew: team.pitCrew ? { ...team.pitCrew } : team.pitCrew }))
68
+ : pitLane.teams,
69
+ };
70
+ }
@@ -1,5 +1,4 @@
1
1
  import { clamp, wrapDistance } from '../simMath.js';
2
- import { PIT_LANE_WIDTH } from './trackConstants.js';
3
2
  import { nearestPointOnPolyline, nearestPointOnPolylineInto, pointInsideBounds, pointIsInsidePolygon } from './trackMath.js';
4
3
  import { queryPitBoxCandidates, queryPitRoadSegmentCandidatesByRoute } from './trackQueryIndex.js';
5
4
  import { ensureQueryScratch } from './trackQueryScratch.js';
@@ -0,0 +1,121 @@
1
+ import { PIT_BOX_COUNT, PIT_BOX_DEPTH, PIT_BOX_LENGTH, PIT_BOX_PAIR_GAP, PIT_BOX_TO_LANE_GAP, PIT_LANE_WIDTH, PIT_SERVICE_AREA_DEPTH, PIT_SERVICE_AREA_LENGTH, PIT_SERVICE_QUEUE_GAP, PIT_TEAM_COUNT, PIT_TEAM_GAP, PIT_BOXES_PER_TEAM, PIT_WORKING_LANE_GAP, PIT_WORKING_LANE_WIDTH } from './trackConstants.js';
2
+ import { interpolatePitPoint, projectPitPoint } from './trackMath.js';
3
+
4
+ export function getPitBoxRunLength({
5
+ teamCount = PIT_TEAM_COUNT,
6
+ boxesPerTeam = PIT_BOXES_PER_TEAM,
7
+ boxLength = PIT_BOX_LENGTH,
8
+ pairGap = PIT_BOX_PAIR_GAP,
9
+ teamGap = PIT_TEAM_GAP,
10
+ } = {}) {
11
+ const boxCount = teamCount * boxesPerTeam;
12
+ return (
13
+ boxCount * boxLength +
14
+ teamCount * Math.max(0, boxesPerTeam - 1) * pairGap +
15
+ Math.max(0, teamCount - 1) * teamGap
16
+ );
17
+ }
18
+
19
+ export function createPitBoxCorners(center, forward, serviceNormal) {
20
+ return createPitRectangleCorners(center, forward, serviceNormal, PIT_BOX_LENGTH, PIT_BOX_DEPTH);
21
+ }
22
+
23
+ export function createPitRectangleCorners(center, forward, serviceNormal, length, depth) {
24
+ const halfLength = length / 2;
25
+ const halfDepth = depth / 2;
26
+ return [
27
+ {
28
+ x: center.x + forward.x * halfLength + serviceNormal.x * halfDepth,
29
+ y: center.y + forward.y * halfLength + serviceNormal.y * halfDepth,
30
+ },
31
+ {
32
+ x: center.x - forward.x * halfLength + serviceNormal.x * halfDepth,
33
+ y: center.y - forward.y * halfLength + serviceNormal.y * halfDepth,
34
+ },
35
+ {
36
+ x: center.x - forward.x * halfLength - serviceNormal.x * halfDepth,
37
+ y: center.y - forward.y * halfLength - serviceNormal.y * halfDepth,
38
+ },
39
+ {
40
+ x: center.x + forward.x * halfLength - serviceNormal.x * halfDepth,
41
+ y: center.y + forward.y * halfLength - serviceNormal.y * halfDepth,
42
+ },
43
+ ];
44
+ }
45
+
46
+ export function createPitBoxes({ laneStart, laneForward, laneLength, serviceNormal }) {
47
+ const runLength = getPitBoxRunLength();
48
+ let cursor = Math.max(PIT_SERVICE_QUEUE_GAP + PIT_BOX_LENGTH / 2, (laneLength - runLength) / 2);
49
+ const workingLaneOffset = PIT_LANE_WIDTH / 2 + PIT_WORKING_LANE_GAP + PIT_WORKING_LANE_WIDTH / 2;
50
+ const boxLateral = workingLaneOffset + PIT_WORKING_LANE_WIDTH / 2 + PIT_BOX_TO_LANE_GAP + PIT_BOX_DEPTH / 2;
51
+ const boxes = [];
52
+ const serviceAreas = [];
53
+
54
+ for (let index = 0; index < PIT_BOX_COUNT; index += 1) {
55
+ const distanceAlongLane = cursor + PIT_BOX_LENGTH / 2;
56
+ const laneTarget = interpolatePitPoint(laneStart, laneForward, distanceAlongLane);
57
+ const center = {
58
+ x: laneTarget.x + serviceNormal.x * boxLateral,
59
+ y: laneTarget.y + serviceNormal.y * boxLateral,
60
+ };
61
+ const teamIndex = Math.floor(index / PIT_BOXES_PER_TEAM);
62
+ const teamBoxIndex = index % PIT_BOXES_PER_TEAM;
63
+
64
+ boxes.push({
65
+ id: `team-${teamIndex + 1}-box-${teamBoxIndex + 1}`,
66
+ index,
67
+ teamIndex,
68
+ teamBoxIndex,
69
+ distanceAlongLane,
70
+ laneTarget,
71
+ center,
72
+ length: PIT_BOX_LENGTH,
73
+ depth: PIT_BOX_DEPTH,
74
+ corners: createPitBoxCorners(center, laneForward, serviceNormal),
75
+ });
76
+
77
+ if (teamBoxIndex === PIT_BOXES_PER_TEAM - 1) {
78
+ const firstTeamBox = boxes[index - (PIT_BOXES_PER_TEAM - 1)];
79
+ const serviceDistance = (firstTeamBox.distanceAlongLane + distanceAlongLane) / 2;
80
+ const queueDistance = Math.max(PIT_SERVICE_QUEUE_GAP / 2, serviceDistance - PIT_SERVICE_QUEUE_GAP);
81
+ const serviceCenter = projectPitPoint(
82
+ laneStart,
83
+ laneForward,
84
+ serviceNormal,
85
+ serviceDistance,
86
+ workingLaneOffset,
87
+ );
88
+ const queuePoint = projectPitPoint(
89
+ laneStart,
90
+ laneForward,
91
+ serviceNormal,
92
+ queueDistance,
93
+ workingLaneOffset,
94
+ );
95
+
96
+ serviceAreas.push({
97
+ id: `team-${teamIndex + 1}-service`,
98
+ index: teamIndex,
99
+ teamIndex,
100
+ distanceAlongLane: serviceDistance,
101
+ queueDistanceAlongLane: queueDistance,
102
+ laneTarget: serviceCenter,
103
+ center: serviceCenter,
104
+ queuePoint,
105
+ length: PIT_SERVICE_AREA_LENGTH,
106
+ depth: PIT_SERVICE_AREA_DEPTH,
107
+ corners: createPitRectangleCorners(serviceCenter, laneForward, serviceNormal, PIT_SERVICE_AREA_LENGTH, PIT_SERVICE_AREA_DEPTH),
108
+ queueCorners: createPitRectangleCorners(queuePoint, laneForward, serviceNormal, PIT_SERVICE_AREA_LENGTH * 0.72, PIT_SERVICE_AREA_DEPTH),
109
+ garageBoxIds: boxes
110
+ .slice(index - (PIT_BOXES_PER_TEAM - 1), index + 1)
111
+ .map((box) => box.id),
112
+ });
113
+ }
114
+
115
+ cursor += PIT_BOX_LENGTH;
116
+ if (teamBoxIndex === 0) cursor += PIT_BOX_PAIR_GAP;
117
+ else if (teamIndex < PIT_TEAM_COUNT - 1) cursor += PIT_TEAM_GAP;
118
+ }
119
+
120
+ return { boxes, serviceAreas, workingLaneOffset };
121
+ }
@@ -0,0 +1,77 @@
1
+ import { TRACK } from './trackConstants.js';
2
+ import { buildTrackModel } from './trackBuilder.js';
3
+ import { generateCenterlineControls, generateFallbackCenterlineControls, generateSafeFallbackCenterlineControls, normalizeSeed } from './proceduralCenterline.js';
4
+ import { isValidProceduralTrackModel } from './trackValidation.js';
5
+ import { resolveProceduralTrackOptions } from './trackGenerationOptions.js';
6
+ import { cacheProceduralTrackDefinition, getCachedProceduralTrack } from './trackModelCache.js';
7
+
8
+ export function createProceduralTrack(seed = Date.now(), options = {}) {
9
+ const normalizedSeed = normalizeSeed(seed);
10
+ const generationOptions = resolveProceduralTrackOptions(options);
11
+ const cacheKey = `${normalizedSeed}:${generationOptions.cacheKey}`;
12
+ const cached = getCachedProceduralTrack(cacheKey);
13
+ if (cached) return cached;
14
+
15
+ for (let attempt = 0; attempt < generationOptions.attempts.primary; attempt += 1) {
16
+ const candidateSeed = (normalizedSeed + Math.imul(attempt, 2654435761)) >>> 0;
17
+ const candidate = {
18
+ ...TRACK,
19
+ name: `Generated GP ${candidateSeed.toString(36).toUpperCase().padStart(6, '0').slice(-6)}`,
20
+ seed: candidateSeed,
21
+ curveInterpolation: 'centripetal',
22
+ centerlineControls: generateCenterlineControls(candidateSeed, generationOptions.shape),
23
+ drsZones: null,
24
+ generationOptions,
25
+ ...(generationOptions.pitLane.enabled === false ? { pitLane: { enabled: false } } : {}),
26
+ };
27
+ const model = buildTrackModel(candidate);
28
+ if (isValidProceduralTrackModel(model, generationOptions)) {
29
+ const trackDefinition = {
30
+ ...candidate,
31
+ drsZones: model.drsZones.map(({ id, startRatio, endRatio }) => ({ id, startRatio, endRatio })),
32
+ };
33
+ return cacheProceduralTrackDefinition(cacheKey, trackDefinition, model);
34
+ }
35
+ }
36
+
37
+ let fallback = null;
38
+ let fallbackModel = null;
39
+ for (let attempt = 0; attempt < generationOptions.attempts.fallback; attempt += 1) {
40
+ const candidateSeed = (normalizedSeed ^ Math.imul(attempt + 1, 2246822519)) >>> 0;
41
+ const candidate = {
42
+ ...TRACK,
43
+ name: `Generated GP ${normalizedSeed.toString(36).toUpperCase().padStart(6, '0').slice(-6)}`,
44
+ seed: candidateSeed,
45
+ curveInterpolation: 'centripetal',
46
+ centerlineControls: generateFallbackCenterlineControls(candidateSeed, generationOptions.shape),
47
+ drsZones: null,
48
+ generationOptions,
49
+ ...(generationOptions.pitLane.enabled === false ? { pitLane: { enabled: false } } : {}),
50
+ };
51
+ const model = buildTrackModel(candidate);
52
+ if (isValidProceduralTrackModel(model, generationOptions)) {
53
+ fallback = candidate;
54
+ fallbackModel = model;
55
+ break;
56
+ }
57
+ }
58
+
59
+ if (!fallback || !fallbackModel) {
60
+ fallback = {
61
+ ...TRACK,
62
+ name: `Generated GP ${normalizedSeed.toString(36).toUpperCase().padStart(6, '0').slice(-6)}`,
63
+ seed: normalizedSeed,
64
+ curveInterpolation: 'centripetal',
65
+ centerlineControls: generateSafeFallbackCenterlineControls(normalizedSeed, generationOptions.shape),
66
+ drsZones: null,
67
+ generationOptions,
68
+ ...(generationOptions.pitLane.enabled === false ? { pitLane: { enabled: false } } : {}),
69
+ };
70
+ fallbackModel = buildTrackModel(fallback);
71
+ }
72
+ const fallbackDefinition = {
73
+ ...fallback,
74
+ drsZones: fallbackModel.drsZones.map(({ id, startRatio, endRatio }) => ({ id, startRatio, endRatio })),
75
+ };
76
+ return cacheProceduralTrackDefinition(cacheKey, fallbackDefinition, fallbackModel);
77
+ }