@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
@@ -1,3 +1,4 @@
1
+ import { clearCurrentDrsState } from '../timing/drsState.js';
1
2
  import { markCarDnf } from './retirements.js';
2
3
  import { simSpeedToKph } from '../units.js';
3
4
  import { freezeVehicleMotion } from '../vehicle/vehicleKinematics.js';
@@ -66,10 +67,7 @@ export function freezeRetiredCar(car, { physicsMode = 'arcade' } = {}) {
66
67
  stabilityState: 'destroyed',
67
68
  });
68
69
  car.canAttack = false;
69
- car.drsActive = false;
70
- car.drsEligible = false;
71
- car.drsZoneId = null;
72
- car.drsZoneEnabled = false;
70
+ clearCurrentDrsState(car);
73
71
  }
74
72
 
75
73
  function resetStallTimer(car) {
@@ -1,11 +1,7 @@
1
1
  import {
2
2
  applyUnservedServicePenalty,
3
- cancelPenaltyRecord,
4
- createPenaltyEvent,
5
- createPenaltyRecord,
6
- servePenaltyRecord,
7
3
  } from './rules/penaltyLedger.js';
8
- import { createPenaltyCancelledEvent, createPenaltyServedEvent } from './rules/penaltyEvents.js';
4
+ import { recordPenaltyForSimulation, servePenaltyForSimulation, cancelPenaltyForSimulation } from './rules/penaltyActions.js';
9
5
  import { buildPenaltyStatsByDriver, getPenaltyStats } from './rules/penaltyStats.js';
10
6
  import {
11
7
  reviewCollisionForSimulation,
@@ -93,7 +89,6 @@ import { applyGridDropForSimulation } from './race/gridPenalties.js';
93
89
  import { initializeRaceSimulation, normalizePitIntentForRace } from './race/raceSetup.js';
94
90
  import { runRaceStep } from './race/raceStep.js';
95
91
  import {
96
- createVehicleSnapshotDependencies,
97
92
  getRaceWinnerSnapshot as getRaceWinnerSnapshotForSimulation,
98
93
  snapshotRace,
99
94
  snapshotRaceObservation,
@@ -209,7 +204,7 @@ export class F1RaceSimulation {
209
204
  }
210
205
 
211
206
  isCarInActivePitStop(car) {
212
- return isCarInActivePitStopForSimulation(this, car);
207
+ return isCarInActivePitStopForSimulation(car);
213
208
  }
214
209
 
215
210
  schedulePitStopAtNextEntry(car, stop) {
@@ -249,7 +244,7 @@ export class F1RaceSimulation {
249
244
  }
250
245
 
251
246
  isPitServiceAreaOccupied(candidate, box) {
252
- return isPitServiceAreaOccupiedForSimulation(this, candidate, box);
247
+ return isPitServiceAreaOccupiedForSimulation(candidate, box);
253
248
  }
254
249
 
255
250
  isPitServiceQueueOccupied(car, box) {
@@ -317,38 +312,15 @@ export class F1RaceSimulation {
317
312
  }
318
313
 
319
314
  recordPenalty(penalty) {
320
- const car = this.cars.find((item) => item.id === penalty.driverId);
321
- const entry = createPenaltyRecord({
322
- sequence: this.nextPenaltyId,
323
- time: this.time,
324
- lap: this.computeLap(car?.raceDistance ?? 0),
325
- penalty,
326
- });
327
- this.nextPenaltyId += 1;
328
- this.penalties.push(entry);
329
- if (entry.gridDrop > 0 && this.raceControl.mode === 'pre-start') {
330
- this.applyGridDrop(entry.driverId, entry.gridDrop);
331
- }
332
- this.events.unshift(createPenaltyEvent(entry));
333
- return entry;
315
+ return recordPenaltyForSimulation(this, penalty);
334
316
  }
335
317
 
336
318
  servePenalty(penaltyId) {
337
- const penalty = this.penalties.find((entry) => entry.id === penaltyId);
338
- const result = servePenaltyRecord(penalty, this.time);
339
- if (result) {
340
- this.events.unshift(createPenaltyServedEvent(result, this.time));
341
- }
342
- return result;
319
+ return servePenaltyForSimulation(this, penaltyId);
343
320
  }
344
321
 
345
322
  cancelPenalty(penaltyId) {
346
- const penalty = this.penalties.find((entry) => entry.id === penaltyId);
347
- const result = cancelPenaltyRecord(penalty, this.time);
348
- if (result) {
349
- this.events.unshift(createPenaltyCancelledEvent(result, this.time));
350
- }
351
- return result;
323
+ return cancelPenaltyForSimulation(this, penaltyId);
352
324
  }
353
325
 
354
326
  getDriverPenaltySeconds(driverId) {
@@ -389,10 +361,6 @@ export class F1RaceSimulation {
389
361
  reviewPitLaneSpeedingForSimulation(this);
390
362
  }
391
363
 
392
- vehicleSnapshotDependencies() {
393
- return createVehicleSnapshotDependencies(this);
394
- }
395
-
396
364
  snapshot() {
397
365
  return snapshotRace(this);
398
366
  }
@@ -11,25 +11,17 @@ export function normalizeReplayGhosts(replayGhosts = []) {
11
11
  }
12
12
 
13
13
  export function updateReplayGhosts(replayGhosts = [], timeSeconds = 0) {
14
- replayGhosts.forEach((ghost) => {
15
- const previous = {
16
- x: ghost.x,
17
- y: ghost.y,
18
- heading: ghost.heading,
19
- };
20
- const sample = interpolateTrajectory(ghost.trajectory, timeSeconds);
21
- Object.assign(ghost, {
22
- previousX: Number.isFinite(previous.x) ? previous.x : sample.x,
23
- previousY: Number.isFinite(previous.y) ? previous.y : sample.y,
24
- previousHeading: Number.isFinite(previous.heading) ? previous.heading : sample.heading,
25
- x: sample.x,
26
- y: sample.y,
27
- heading: sample.heading,
28
- speedKph: sample.speedKph,
29
- progressMeters: sample.progressMeters,
30
- timeSeconds,
31
- });
32
- });
14
+ for (let index = 0; index < replayGhosts.length; index += 1) {
15
+ const ghost = replayGhosts[index];
16
+ const previousX = ghost.x;
17
+ const previousY = ghost.y;
18
+ const previousHeading = ghost.heading;
19
+ interpolateTrajectoryInto(ghost, ghost.trajectory, timeSeconds);
20
+ ghost.previousX = Number.isFinite(previousX) ? previousX : ghost.x;
21
+ ghost.previousY = Number.isFinite(previousY) ? previousY : ghost.y;
22
+ ghost.previousHeading = Number.isFinite(previousHeading) ? previousHeading : ghost.heading;
23
+ ghost.timeSeconds = timeSeconds;
24
+ }
33
25
  }
34
26
 
35
27
  export function serializeReplayGhosts(replayGhosts = []) {
@@ -57,7 +49,7 @@ function normalizeReplayGhost(ghost, index) {
57
49
  const trajectory = normalizeTrajectory(ghost.trajectory);
58
50
  if (trajectory.length === 0) return null;
59
51
  const id = String(ghost.id ?? `replay-ghost-${index + 1}`);
60
- const initial = interpolateTrajectory(trajectory, 0);
52
+ const initial = interpolateTrajectoryInto({}, trajectory, 0);
61
53
  return {
62
54
  id,
63
55
  label: String(ghost.label ?? id),
@@ -103,23 +95,43 @@ function normalizeTrajectory(trajectory) {
103
95
  .sort((a, b) => a.timeSeconds - b.timeSeconds);
104
96
  }
105
97
 
106
- function interpolateTrajectory(trajectory, timeSeconds) {
107
- if (trajectory.length === 1 || timeSeconds <= trajectory[0].timeSeconds) return { ...trajectory[0] };
98
+ function interpolateTrajectoryInto(target, trajectory, timeSeconds) {
99
+ if (trajectory.length === 1 || timeSeconds <= trajectory[0].timeSeconds) {
100
+ return writeTrajectorySample(target, trajectory[0]);
101
+ }
108
102
  const last = trajectory[trajectory.length - 1];
109
- if (timeSeconds >= last.timeSeconds) return { ...last };
110
- const nextIndex = trajectory.findIndex((sample) => sample.timeSeconds >= timeSeconds);
103
+ if (timeSeconds >= last.timeSeconds) return writeTrajectorySample(target, last);
104
+ const nextIndex = lowerBoundTrajectoryTime(trajectory, timeSeconds);
111
105
  const previous = trajectory[Math.max(0, nextIndex - 1)];
112
106
  const next = trajectory[nextIndex];
113
107
  const span = next.timeSeconds - previous.timeSeconds || 1;
114
108
  const amount = clamp((timeSeconds - previous.timeSeconds) / span, 0, 1);
115
- return {
116
- timeSeconds,
117
- x: lerp(previous.x, next.x, amount),
118
- y: lerp(previous.y, next.y, amount),
119
- heading: previous.heading + normalizeAngle(next.heading - previous.heading) * amount,
120
- speedKph: lerp(previous.speedKph, next.speedKph, amount),
121
- progressMeters: lerp(previous.progressMeters, next.progressMeters, amount),
122
- };
109
+ target.x = lerp(previous.x, next.x, amount);
110
+ target.y = lerp(previous.y, next.y, amount);
111
+ target.heading = previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
112
+ target.speedKph = lerp(previous.speedKph, next.speedKph, amount);
113
+ target.progressMeters = lerp(previous.progressMeters, next.progressMeters, amount);
114
+ return target;
115
+ }
116
+
117
+ function lowerBoundTrajectoryTime(trajectory, timeSeconds) {
118
+ let low = 0;
119
+ let high = trajectory.length;
120
+ while (low < high) {
121
+ const middle = Math.floor((low + high) / 2);
122
+ if (trajectory[middle].timeSeconds < timeSeconds) low = middle + 1;
123
+ else high = middle;
124
+ }
125
+ return low;
126
+ }
127
+
128
+ function writeTrajectorySample(target, sample) {
129
+ target.x = sample.x;
130
+ target.y = sample.y;
131
+ target.heading = sample.heading;
132
+ target.speedKph = sample.speedKph;
133
+ target.progressMeters = sample.progressMeters;
134
+ return target;
123
135
  }
124
136
 
125
137
  function finiteNumber(value) {
@@ -1,19 +1,5 @@
1
1
  import { simSpeedToKph } from '../units.js';
2
2
 
3
- export function calculateCollisionPenalties({ first, second, collision = null, rule }) {
4
- const penalties = [];
5
- emitCollisionPenalties({
6
- first,
7
- second,
8
- collision,
9
- rule,
10
- emit: (penalty) => {
11
- penalties.push(penalty);
12
- },
13
- });
14
- return penalties;
15
- }
16
-
17
3
  export function emitCollisionPenalties({ first, second, collision = null, rule, emit, emitContext = null }) {
18
4
  if (!rule || typeof emit !== 'function') return 0;
19
5
  const severity = collision?.depth ?? 0;
@@ -0,0 +1,37 @@
1
+ import { cancelPenaltyRecord, createPenaltyEvent, createPenaltyRecord, servePenaltyRecord } from './penaltyLedger.js';
2
+ import { createPenaltyCancelledEvent, createPenaltyServedEvent } from './penaltyEvents.js';
3
+
4
+ export function recordPenaltyForSimulation(sim, penalty) {
5
+ const car = sim.cars.find((item) => item.id === penalty.driverId);
6
+ const entry = createPenaltyRecord({
7
+ sequence: sim.nextPenaltyId,
8
+ time: sim.time,
9
+ lap: sim.computeLap(car?.raceDistance ?? 0),
10
+ penalty,
11
+ });
12
+ sim.nextPenaltyId += 1;
13
+ sim.penalties.push(entry);
14
+ if (entry.gridDrop > 0 && sim.raceControl.mode === 'pre-start') {
15
+ sim.applyGridDrop(entry.driverId, entry.gridDrop);
16
+ }
17
+ sim.events.unshift(createPenaltyEvent(entry));
18
+ return entry;
19
+ }
20
+
21
+ export function servePenaltyForSimulation(sim, penaltyId) {
22
+ const penalty = sim.penalties.find((entry) => entry.id === penaltyId);
23
+ const result = servePenaltyRecord(penalty, sim.time);
24
+ if (result) {
25
+ sim.events.unshift(createPenaltyServedEvent(result, sim.time));
26
+ }
27
+ return result;
28
+ }
29
+
30
+ export function cancelPenaltyForSimulation(sim, penaltyId) {
31
+ const penalty = sim.penalties.find((entry) => entry.id === penaltyId);
32
+ const result = cancelPenaltyRecord(penalty, sim.time);
33
+ if (result) {
34
+ sim.events.unshift(createPenaltyCancelledEvent(result, sim.time));
35
+ }
36
+ return result;
37
+ }
@@ -48,7 +48,14 @@ export function createPenaltyEvent(entry) {
48
48
  }
49
49
 
50
50
  export function serializePenalty(penalty) {
51
- return { ...penalty };
51
+ return {
52
+ ...penalty,
53
+ ...(Array.isArray(penalty.consequences) ? {
54
+ consequences: penalty.consequences.map((consequence) => ({ ...consequence })),
55
+ } : {}),
56
+ ...(Array.isArray(penalty.usedCompounds) ? { usedCompounds: [...penalty.usedCompounds] } : {}),
57
+ ...(Array.isArray(penalty.wheelOffsets) ? { wheelOffsets: [...penalty.wheelOffsets] } : {}),
58
+ };
52
59
  }
53
60
 
54
61
  export function servePenaltyRecord(penalty, time) {
@@ -5,6 +5,7 @@ import { calculatePitLaneSpeedingReview } from './pitLaneSpeedingSteward.js';
5
5
  import { getPenaltyRule } from '../rulesConfig.js';
6
6
  import { simSpeedToKph } from '../units.js';
7
7
  import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
8
+ import { progressDelta } from '../race/raceDistance.js';
8
9
 
9
10
  function velocityComponentX(car) {
10
11
  return Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)
@@ -18,13 +19,6 @@ function velocityComponentY(car) {
18
19
  : Math.sin(car.heading) * car.speed;
19
20
  }
20
21
 
21
- function progressDelta(a, b, trackLength) {
22
- let delta = a - b;
23
- if (delta < -trackLength / 2) delta += trackLength;
24
- if (delta > trackLength / 2) delta -= trackLength;
25
- return delta;
26
- }
27
-
28
22
  function prepareCollisionStewardContext(scratch = null) {
29
23
  const target = scratch
30
24
  ? (scratch.collisionStewardContext ??= { sharedFaultDriverIds: [] })
@@ -32,3 +32,7 @@ export function seededRange(random, min, max) {
32
32
  export function wrapDistance(value, length) {
33
33
  return ((value % length) + length) % length;
34
34
  }
35
+
36
+ export function finiteOrNull(value) {
37
+ return Number.isFinite(value) ? value : null;
38
+ }
@@ -38,12 +38,8 @@ export function createVehicleSnapshotDependencies(sim) {
38
38
  normalizePitIntent,
39
39
  PIT_INTENT_NONE,
40
40
  serializeLapTelemetry,
41
- serializeObservationPitStop: (pitStop) => serializeObservationPitStop(
42
- pitStop,
43
- normalizePitIntent,
44
- PIT_INTENT_NONE,
45
- ),
46
- serializePitStop: (pitStop) => serializePitStop(pitStop, normalizePitIntent, PIT_INTENT_NONE),
41
+ serializeObservationPitStop,
42
+ serializePitStop,
47
43
  serializeRenderPitStop,
48
44
  simSpeedToKph,
49
45
  simUnitsToMeters,
@@ -220,11 +216,34 @@ function carsForSnapshot(sim, ordered) {
220
216
  return [...ranked, ...unranked];
221
217
  }
222
218
 
219
+ const RENDER_CAR_ORDER_SCRATCH = new WeakMap();
220
+
223
221
  function renderCarsForSnapshotInto(targetCars, sim, ordered, dependencies) {
224
- const entries = carsForSnapshot(sim, ordered);
225
- targetCars.length = entries.length;
226
- entries.forEach(({ car }, index) => {
227
- targetCars[index] = serializeRenderCarInto(targetCars[index] ?? {}, car, dependencies);
228
- });
222
+ let scratch = RENDER_CAR_ORDER_SCRATCH.get(targetCars);
223
+ if (!scratch) {
224
+ scratch = { orderedIds: new Set(), unranked: [] };
225
+ RENDER_CAR_ORDER_SCRATCH.set(targetCars, scratch);
226
+ }
227
+ const { orderedIds, unranked } = scratch;
228
+ orderedIds.clear();
229
+ unranked.length = 0;
230
+ let count = 0;
231
+ for (const car of ordered) {
232
+ orderedIds.add(car.id);
233
+ targetCars[count] = serializeRenderCarInto(targetCars[count] ?? {}, car, dependencies);
234
+ count += 1;
235
+ }
236
+ for (const car of sim.cars) {
237
+ if (!orderedIds.has(car.id)) unranked.push(car);
238
+ }
239
+ unranked.sort((a, b) => a.index - b.index);
240
+ for (const car of unranked) {
241
+ targetCars[count] = serializeRenderCarInto(targetCars[count] ?? {}, car, dependencies);
242
+ count += 1;
243
+ }
244
+ targetCars.length = count;
245
+ // Scratch owns ordering only; do not retain live participants between frames.
246
+ orderedIds.clear();
247
+ unranked.length = 0;
229
248
  return targetCars;
230
249
  }
@@ -0,0 +1,8 @@
1
+ // Suppress the current latch without erasing passage history or next-zone hints.
2
+ // External placement uses the separate full reset in vehicleState.js.
3
+ export function clearCurrentDrsState(car) {
4
+ car.drsActive = false;
5
+ car.drsEligible = false;
6
+ car.drsZoneId = null;
7
+ car.drsZoneEnabled = false;
8
+ }
@@ -1,3 +1,4 @@
1
+ import { clearCurrentDrsState } from './drsState.js';
1
2
  import { wrapDistance } from '../simMath.js';
2
3
 
3
4
  function wrapProgress(value, length) {
@@ -106,10 +107,7 @@ function updateDrsLatchCore(
106
107
  runtimeBenchmarkStats = null,
107
108
  ) {
108
109
  if (safetyCarDeployed || car.finished) {
109
- car.drsEligible = false;
110
- car.drsActive = false;
111
- car.drsZoneId = null;
112
- car.drsZoneEnabled = false;
110
+ clearCurrentDrsState(car);
113
111
  car._drsZoneRef = null;
114
112
  return;
115
113
  }
@@ -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';
@@ -108,9 +111,6 @@ export function updateSectorPerformance(cars, stats = null) {
108
111
  return true;
109
112
  }
110
113
 
111
- export function finiteOrNull(value) {
112
- return Number.isFinite(value) ? value : null;
113
- }
114
114
 
115
115
  function classifySectorPerformance(value, personalBest, overallBest) {
116
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 = [