@inventure71/paddockjs 0.3.0 → 2.0.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 (202) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/INSTALL_AND_UPDATE.md +5 -2
  3. package/README.md +328 -13
  4. package/docs/architecture.md +150 -41
  5. package/docs/concepts.md +22 -3
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +474 -34
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +1 -0
  10. package/docs/learnings.md +23 -1
  11. package/docs/rules.md +169 -21
  12. package/docs/system_specs.md +56 -20
  13. package/docs/training.md +369 -25
  14. package/examples/python/README.md +62 -0
  15. package/examples/python/base_policy_server.py +191 -0
  16. package/examples/python/requirements.txt +2 -0
  17. package/examples/train-basic-policy.mjs +5 -6
  18. package/examples/trainingData.mjs +133 -0
  19. package/package.json +15 -4
  20. package/src/api/PaddockSimulatorController.js +36 -2
  21. package/src/app/BrowserExpertAdapter.js +367 -9
  22. package/src/app/F1SimulatorApp.js +615 -940
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +14 -1
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +24 -4
  48. package/src/environment/actions.js +38 -7
  49. package/src/environment/controllerLoop.js +246 -0
  50. package/src/environment/episode.js +86 -2
  51. package/src/environment/evaluation.js +206 -0
  52. package/src/environment/index.d.ts +638 -6
  53. package/src/environment/index.js +14 -0
  54. package/src/environment/metrics.js +159 -0
  55. package/src/environment/observationOptions.js +33 -0
  56. package/src/environment/observations.js +500 -43
  57. package/src/environment/options.js +41 -2
  58. package/src/environment/recorder.js +28 -0
  59. package/src/environment/runtime.js +260 -24
  60. package/src/environment/scenarios.js +249 -0
  61. package/src/environment/sensors/bodySenses.js +23 -0
  62. package/src/environment/sensors/boundarySenses.js +17 -0
  63. package/src/environment/sensors/carRays.js +75 -0
  64. package/src/environment/sensors/contactSenses.js +32 -0
  65. package/src/environment/sensors/index.js +157 -0
  66. package/src/environment/sensors/indexedRayBands.js +103 -0
  67. package/src/environment/sensors/nearbyCars.js +52 -0
  68. package/src/environment/sensors/opponentRadar.js +65 -0
  69. package/src/environment/sensors/rayConfig.js +110 -0
  70. package/src/environment/sensors/rayDefaults.js +6 -0
  71. package/src/environment/sensors/rayGeometry.js +60 -0
  72. package/src/environment/sensors/rayGuards.js +19 -0
  73. package/src/environment/sensors/sensorTargets.js +60 -0
  74. package/src/environment/sensors/surfaceRays.js +296 -0
  75. package/src/environment/sensors/trackRays.js +261 -0
  76. package/src/environment/sensors.js +1 -257
  77. package/src/environment/specs.js +188 -9
  78. package/src/environment/workerProtocol.js +64 -0
  79. package/src/index.d.ts +533 -7
  80. package/src/index.js +26 -0
  81. package/src/rendering/proceduralTrackAsset.js +20 -510
  82. package/src/rendering/renderSnapshot.js +38 -12
  83. package/src/rendering/track/finishLineRenderer.js +34 -0
  84. package/src/rendering/track/gridRenderer.js +47 -0
  85. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  86. package/src/rendering/track/pitLaneRenderer.js +248 -0
  87. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  88. package/src/rendering/track/trackRenderConstants.js +38 -0
  89. package/src/rendering/track/trackRenderGeometry.js +94 -0
  90. package/src/simulation/collisionGeometry.js +214 -0
  91. package/src/simulation/driver/driverControlConstants.js +28 -0
  92. package/src/simulation/driver/driverController.js +47 -0
  93. package/src/simulation/driver/driverInput.js +28 -0
  94. package/src/simulation/driver/driverMath.js +6 -0
  95. package/src/simulation/driver/driverPersonality.js +25 -0
  96. package/src/simulation/driver/edgeRecovery.js +48 -0
  97. package/src/simulation/driver/racingControls.js +399 -0
  98. package/src/simulation/driver/racingLinePlan.js +166 -0
  99. package/src/simulation/driver/recoveryDynamics.js +38 -0
  100. package/src/simulation/driver/rejoinControls.js +213 -0
  101. package/src/simulation/driver/safetyCarControls.js +35 -0
  102. package/src/simulation/driver/trafficScan.js +51 -0
  103. package/src/simulation/driverController.js +4 -253
  104. package/src/simulation/participants/participantInteractions.js +120 -0
  105. package/src/simulation/pit/pitFlow.js +234 -0
  106. package/src/simulation/pit/pitIntent.js +92 -0
  107. package/src/simulation/pit/pitOccupancy.js +47 -0
  108. package/src/simulation/pit/pitPenaltyService.js +71 -0
  109. package/src/simulation/pit/pitQueue.js +55 -0
  110. package/src/simulation/pit/pitRouteMovement.js +160 -0
  111. package/src/simulation/pit/pitRouting.js +319 -0
  112. package/src/simulation/pit/pitService.js +41 -0
  113. package/src/simulation/pit/pitServiceConstants.js +21 -0
  114. package/src/simulation/pit/pitServiceProfile.js +45 -0
  115. package/src/simulation/pit/pitSnapshots.js +61 -0
  116. package/src/simulation/pit/pitState.js +113 -0
  117. package/src/simulation/pit/pitTireService.js +92 -0
  118. package/src/simulation/race/classification.js +104 -0
  119. package/src/simulation/race/gridPenalties.js +37 -0
  120. package/src/simulation/race/raceControlState.js +38 -0
  121. package/src/simulation/race/raceDistance.js +32 -0
  122. package/src/simulation/race/raceFinish.js +65 -0
  123. package/src/simulation/race/raceLifecycle.js +71 -0
  124. package/src/simulation/race/raceOrder.js +113 -0
  125. package/src/simulation/race/raceProgress.js +124 -0
  126. package/src/simulation/race/raceSetup.js +93 -0
  127. package/src/simulation/race/raceStep.js +80 -0
  128. package/src/simulation/race/redFlag.js +15 -0
  129. package/src/simulation/race/retirements.js +44 -0
  130. package/src/simulation/race/safetyCar.js +86 -0
  131. package/src/simulation/race/stalledDnf.js +86 -0
  132. package/src/simulation/raceSimulation.js +386 -1012
  133. package/src/simulation/replay/replayGhosts.js +128 -0
  134. package/src/simulation/rules/collisionSteward.js +69 -0
  135. package/src/simulation/rules/moduleConfig.js +72 -0
  136. package/src/simulation/rules/penaltyConfig.js +99 -0
  137. package/src/simulation/rules/penaltyEvents.js +18 -0
  138. package/src/simulation/rules/penaltyLedger.js +153 -0
  139. package/src/simulation/rules/penaltyStats.js +26 -0
  140. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  141. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  142. package/src/simulation/rules/ruleDefaults.js +85 -0
  143. package/src/simulation/rules/rulesReview.js +141 -0
  144. package/src/simulation/rules/rulesetPresets.js +47 -0
  145. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  147. package/src/simulation/rulesConfig.js +34 -0
  148. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryIndex.js +945 -0
  171. package/src/simulation/track/trackSectors.js +19 -0
  172. package/src/simulation/track/trackStatePolicy.js +24 -0
  173. package/src/simulation/track/trackValidation.js +98 -0
  174. package/src/simulation/trackModel.js +1 -628
  175. package/src/simulation/units.js +6 -4
  176. package/src/simulation/vehicle/contactResolution.js +116 -0
  177. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  178. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  179. package/src/simulation/vehicle/runoffResponse.js +88 -0
  180. package/src/simulation/vehicle/surfacePriority.js +28 -0
  181. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  182. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  183. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  184. package/src/simulation/vehicle/vehicleState.js +186 -0
  185. package/src/simulation/vehicle/wheelSurface.js +150 -0
  186. package/src/simulation/vehicleGeometry.js +1 -0
  187. package/src/simulation/vehiclePhysics.js +1 -104
  188. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  189. package/src/simulation/wheelSurface.js +1 -0
  190. package/src/styles.css +342 -42
  191. package/src/ui/bannerTemplates.js +69 -0
  192. package/src/ui/cameraControlsTemplate.js +20 -0
  193. package/src/ui/carOverviewTemplate.js +33 -0
  194. package/src/ui/componentTemplates.js +20 -426
  195. package/src/ui/raceCanvasTemplate.js +69 -0
  196. package/src/ui/raceControlStatusBanner.js +39 -0
  197. package/src/ui/raceControlsTemplate.js +36 -0
  198. package/src/ui/shellTemplate.js +6 -5
  199. package/src/ui/telemetryDrawerTemplate.js +55 -0
  200. package/src/ui/telemetryTemplates.js +136 -0
  201. package/src/ui/templateUtils.js +23 -0
  202. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,395 @@
1
+ import { clamp } from '../simMath.js';
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';
4
+ import { clonePoint, distance, headingVector, interpolatePitPoint, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
5
+ import { findPitAccessConnection, createPitAccessRoadCenterline } from './pitLaneAccess.js';
6
+ import { nearestTrackState, pointAt } from './spatialQueries.js';
7
+
8
+ export function createStartStraightBasis(track) {
9
+ const finish = pointAt(track, 0);
10
+ return {
11
+ finish,
12
+ forward: {
13
+ x: Math.cos(finish.heading),
14
+ y: Math.sin(finish.heading),
15
+ },
16
+ normal: {
17
+ x: finish.normalX,
18
+ y: finish.normalY,
19
+ },
20
+ };
21
+ }
22
+
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
+
38
+ export function createPitLaneLayout() {
39
+ const runLength = getPitBoxRunLength();
40
+ const length = runLength + PIT_LANE_ENTRY_BUFFER + PIT_LANE_EXIT_BUFFER;
41
+ return {
42
+ runLength,
43
+ length,
44
+ entryDistance: -length * PIT_LANE_FINISH_RATIO,
45
+ exitDistance: length * (1 - PIT_LANE_FINISH_RATIO),
46
+ entryBuffer: PIT_LANE_ENTRY_BUFFER,
47
+ exitBuffer: PIT_LANE_EXIT_BUFFER,
48
+ };
49
+ }
50
+
51
+ export function createPitLaneEndpoints(track, side, laneOffset, layout = createPitLaneLayout()) {
52
+ const basis = createStartStraightBasis(track);
53
+ return {
54
+ ...basis,
55
+ start: projectPitPoint(basis.finish, basis.forward, basis.normal, layout.entryDistance, side * laneOffset),
56
+ end: projectPitPoint(basis.finish, basis.forward, basis.normal, layout.exitDistance, side * laneOffset),
57
+ };
58
+ }
59
+
60
+ export function scorePitLanePlacement(track, side, laneOffset, layout) {
61
+ const placement = createPitLaneEndpoints(track, side, laneOffset, layout);
62
+ const entryLanePoint = placement.start;
63
+ const exitLanePoint = placement.end;
64
+ const worldCenter = { x: WORLD.width / 2, y: WORLD.height / 2 };
65
+ const laneMidpoint = {
66
+ x: (entryLanePoint.x + exitLanePoint.x) / 2,
67
+ y: (entryLanePoint.y + exitLanePoint.y) / 2,
68
+ };
69
+ const outwardDistance = distance(laneMidpoint, worldCenter) - distance(placement.finish, worldCenter);
70
+ const laneVector = normalizeVector({
71
+ x: exitLanePoint.x - entryLanePoint.x,
72
+ y: exitLanePoint.y - entryLanePoint.y,
73
+ });
74
+ const serviceNormal = normalizeVector({
75
+ x: placement.normal.x * side,
76
+ y: placement.normal.y * side,
77
+ });
78
+ const workingLaneOffset = PIT_LANE_WIDTH / 2 + PIT_WORKING_LANE_GAP + PIT_WORKING_LANE_WIDTH / 2;
79
+ const boxLateral = workingLaneOffset + PIT_WORKING_LANE_WIDTH / 2 + PIT_BOX_TO_LANE_GAP + PIT_BOX_DEPTH / 2;
80
+ let minimumClearance = Infinity;
81
+ let minimumTrackClearance = Infinity;
82
+
83
+ for (let index = 0; index <= 48; index += 1) {
84
+ const amount = index / 48;
85
+ const lanePoint = interpolatePitPoint(entryLanePoint, laneVector, laneVector.length * amount);
86
+ const boxPoint = {
87
+ x: lanePoint.x + serviceNormal.x * boxLateral,
88
+ y: lanePoint.y + serviceNormal.y * boxLateral,
89
+ };
90
+ minimumClearance = Math.min(minimumClearance, pointWorldClearance(lanePoint), pointWorldClearance(boxPoint));
91
+ minimumTrackClearance = Math.min(
92
+ minimumTrackClearance,
93
+ nearestTrackState(track, lanePoint).crossTrackError -
94
+ (track.width / 2 + (track.kerbWidth ?? 0) + PIT_TRACK_CLEARANCE_MARGIN),
95
+ );
96
+ }
97
+
98
+ return {
99
+ side,
100
+ laneOffset,
101
+ worldClearance: minimumClearance,
102
+ trackClearance: minimumTrackClearance,
103
+ outwardDistance,
104
+ score: minimumTrackClearance * 5 + minimumClearance + outwardDistance * 3,
105
+ };
106
+ }
107
+
108
+ export function choosePitLanePlacement(track, baseLaneOffset, layout) {
109
+ const candidates = [];
110
+ for (let offsetStep = 0; offsetStep <= 9; offsetStep += 1) {
111
+ const laneOffset = baseLaneOffset + offsetStep * PIT_LANE_OFFSET_SEARCH_STEP;
112
+ candidates.push(scorePitLanePlacement(track, -1, laneOffset, layout));
113
+ candidates.push(scorePitLanePlacement(track, 1, laneOffset, layout));
114
+ }
115
+
116
+ const valid = candidates
117
+ .filter((candidate) => candidate.worldClearance > 0 && candidate.trackClearance > 0)
118
+ .sort((left, right) => (
119
+ Number(right.outwardDistance > 0) - Number(left.outwardDistance > 0) ||
120
+ right.trackClearance - left.trackClearance ||
121
+ right.worldClearance - left.worldClearance ||
122
+ left.laneOffset - right.laneOffset
123
+ ));
124
+ if (valid.length) return valid[0];
125
+
126
+ return candidates.sort((left, right) => right.score - left.score)[0];
127
+ }
128
+
129
+ export function createPitBoxCorners(center, forward, serviceNormal) {
130
+ const halfLength = PIT_BOX_LENGTH / 2;
131
+ const halfDepth = PIT_BOX_DEPTH / 2;
132
+ return [
133
+ {
134
+ x: center.x + forward.x * halfLength + serviceNormal.x * halfDepth,
135
+ y: center.y + forward.y * halfLength + serviceNormal.y * halfDepth,
136
+ },
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
+ }
151
+
152
+ export function createPitRectangleCorners(center, forward, serviceNormal, length, depth) {
153
+ const halfLength = length / 2;
154
+ const halfDepth = depth / 2;
155
+ return [
156
+ {
157
+ x: center.x + forward.x * halfLength + serviceNormal.x * halfDepth,
158
+ y: center.y + forward.y * halfLength + serviceNormal.y * halfDepth,
159
+ },
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
+ }
174
+
175
+ export function createPitBoxes({ laneStart, laneForward, laneLength, serviceNormal }) {
176
+ const runLength = getPitBoxRunLength();
177
+ let cursor = Math.max(PIT_SERVICE_QUEUE_GAP + PIT_BOX_LENGTH / 2, (laneLength - runLength) / 2);
178
+ const workingLaneOffset = PIT_LANE_WIDTH / 2 + PIT_WORKING_LANE_GAP + PIT_WORKING_LANE_WIDTH / 2;
179
+ const boxLateral = workingLaneOffset + PIT_WORKING_LANE_WIDTH / 2 + PIT_BOX_TO_LANE_GAP + PIT_BOX_DEPTH / 2;
180
+ const boxes = [];
181
+ const serviceAreas = [];
182
+
183
+ for (let index = 0; index < PIT_BOX_COUNT; index += 1) {
184
+ const distanceAlongLane = cursor + PIT_BOX_LENGTH / 2;
185
+ const laneTarget = interpolatePitPoint(laneStart, laneForward, distanceAlongLane);
186
+ const center = {
187
+ x: laneTarget.x + serviceNormal.x * boxLateral,
188
+ y: laneTarget.y + serviceNormal.y * boxLateral,
189
+ };
190
+ const teamIndex = Math.floor(index / PIT_BOXES_PER_TEAM);
191
+ const teamBoxIndex = index % PIT_BOXES_PER_TEAM;
192
+
193
+ boxes.push({
194
+ id: `team-${teamIndex + 1}-box-${teamBoxIndex + 1}`,
195
+ index,
196
+ teamIndex,
197
+ teamBoxIndex,
198
+ distanceAlongLane,
199
+ laneTarget,
200
+ center,
201
+ length: PIT_BOX_LENGTH,
202
+ depth: PIT_BOX_DEPTH,
203
+ corners: createPitBoxCorners(center, laneForward, serviceNormal),
204
+ });
205
+
206
+ if (teamBoxIndex === PIT_BOXES_PER_TEAM - 1) {
207
+ const firstTeamBox = boxes[index - (PIT_BOXES_PER_TEAM - 1)];
208
+ const serviceDistance = (firstTeamBox.distanceAlongLane + distanceAlongLane) / 2;
209
+ const queueDistance = Math.max(PIT_SERVICE_QUEUE_GAP / 2, serviceDistance - PIT_SERVICE_QUEUE_GAP);
210
+ const serviceCenter = projectPitPoint(
211
+ laneStart,
212
+ laneForward,
213
+ serviceNormal,
214
+ serviceDistance,
215
+ workingLaneOffset,
216
+ );
217
+ const queuePoint = projectPitPoint(
218
+ laneStart,
219
+ laneForward,
220
+ serviceNormal,
221
+ queueDistance,
222
+ workingLaneOffset,
223
+ );
224
+
225
+ serviceAreas.push({
226
+ id: `team-${teamIndex + 1}-service`,
227
+ index: teamIndex,
228
+ teamIndex,
229
+ distanceAlongLane: serviceDistance,
230
+ queueDistanceAlongLane: queueDistance,
231
+ laneTarget: serviceCenter,
232
+ center: serviceCenter,
233
+ queuePoint,
234
+ length: PIT_SERVICE_AREA_LENGTH,
235
+ depth: PIT_SERVICE_AREA_DEPTH,
236
+ corners: createPitRectangleCorners(serviceCenter, laneForward, serviceNormal, PIT_SERVICE_AREA_LENGTH, PIT_SERVICE_AREA_DEPTH),
237
+ queueCorners: createPitRectangleCorners(queuePoint, laneForward, serviceNormal, PIT_SERVICE_AREA_LENGTH * 0.72, PIT_SERVICE_AREA_DEPTH),
238
+ garageBoxIds: boxes
239
+ .slice(index - (PIT_BOXES_PER_TEAM - 1), index + 1)
240
+ .map((box) => box.id),
241
+ });
242
+ }
243
+
244
+ cursor += PIT_BOX_LENGTH;
245
+ if (teamBoxIndex === 0) cursor += PIT_BOX_PAIR_GAP;
246
+ else if (teamIndex < PIT_TEAM_COUNT - 1) cursor += PIT_TEAM_GAP;
247
+ }
248
+
249
+ return { boxes, serviceAreas, workingLaneOffset };
250
+ }
251
+
252
+ export function createPitLaneBounds(pitLane) {
253
+ const points = [
254
+ ...(pitLane.entry?.roadCenterline ?? []),
255
+ ...(pitLane.exit?.roadCenterline ?? []),
256
+ ...(pitLane.mainLane?.points ?? []),
257
+ ...(pitLane.workingLane?.points ?? []),
258
+ ...(pitLane.boxes ?? []).flatMap((box) => box.corners ?? []),
259
+ ...(pitLane.serviceAreas ?? []).flatMap((area) => [
260
+ ...(area.corners ?? []),
261
+ ...(area.queueCorners ?? []),
262
+ ]),
263
+ ];
264
+ const padding = Math.max(
265
+ pitLane.width ?? 0,
266
+ pitLane.workingLane?.width ?? 0,
267
+ PIT_BOX_DEPTH,
268
+ PIT_SERVICE_AREA_DEPTH,
269
+ ) / 2 + metersToSimUnits(3);
270
+ return createPointBounds(points, padding);
271
+ }
272
+
273
+ export function createPitLaneModel(track) {
274
+ const layout = createPitLaneLayout();
275
+ const laneOffset = track.width / 2 + (track.kerbWidth ?? 0) + PIT_LANE_EDGE_GAP + PIT_LANE_WIDTH / 2;
276
+ const placement = choosePitLanePlacement(track, laneOffset, layout);
277
+ const side = placement.side;
278
+ const placedLaneOffset = placement.laneOffset;
279
+ const startStraight = createPitLaneEndpoints(track, side, placedLaneOffset, layout);
280
+ const laneStart = startStraight.start;
281
+ const laneEnd = startStraight.end;
282
+ const laneVector = normalizeVector({
283
+ x: laneEnd.x - laneStart.x,
284
+ y: laneEnd.y - laneStart.y,
285
+ });
286
+ const lateralSpan = Math.max(0, placedLaneOffset - track.width / 2);
287
+ const accessLength = clamp(lateralSpan * 0.9, PIT_ACCESS_MIN_LENGTH, PIT_ACCESS_MAX_LENGTH);
288
+ const serviceNormal = normalizeVector({
289
+ x: startStraight.normal.x * side,
290
+ y: startStraight.normal.y * side,
291
+ });
292
+ const entryConnection = findPitAccessConnection(track, laneStart, laneVector, {
293
+ direction: 'entry',
294
+ startDistance: layout.entryDistance - PIT_ENTRY_SEARCH_BEFORE,
295
+ endDistance: layout.entryDistance + PIT_ENTRY_SEARCH_AFTER,
296
+ fallbackDistance: layout.entryDistance - accessLength,
297
+ });
298
+ const exitConnection = findPitAccessConnection(track, laneEnd, laneVector, {
299
+ direction: 'exit',
300
+ startDistance: layout.exitDistance - PIT_EXIT_SEARCH_BEFORE,
301
+ endDistance: layout.exitDistance + PIT_EXIT_SEARCH_AFTER,
302
+ fallbackDistance: layout.exitDistance + accessLength,
303
+ });
304
+ const entryTangentLength = clamp(
305
+ entryConnection.pathLength * PIT_ACCESS_TANGENT_RATIO,
306
+ PIT_LANE_WIDTH,
307
+ PIT_ACCESS_MAX_LENGTH,
308
+ );
309
+ const exitTangentLength = clamp(
310
+ exitConnection.pathLength * PIT_ACCESS_TANGENT_RATIO,
311
+ PIT_LANE_WIDTH,
312
+ PIT_ACCESS_MAX_LENGTH,
313
+ );
314
+ const entryMergePoint = interpolatePitPoint(laneStart, laneVector, Math.min(metersToSimUnits(86), laneVector.length * 0.16));
315
+ const exitMergePoint = interpolatePitPoint(laneEnd, laneVector, -Math.min(metersToSimUnits(86), laneVector.length * 0.16));
316
+
317
+ const entryRoadCenterline = createPitAccessRoadCenterline(
318
+ entryConnection.trackConnectPoint,
319
+ laneStart,
320
+ headingVector(entryConnection.trackPoint.heading),
321
+ laneVector,
322
+ entryTangentLength,
323
+ );
324
+ const exitRoadCenterline = createPitAccessRoadCenterline(
325
+ laneEnd,
326
+ exitConnection.trackConnectPoint,
327
+ laneVector,
328
+ headingVector(exitConnection.trackPoint.heading),
329
+ exitTangentLength,
330
+ );
331
+ const { boxes, serviceAreas, workingLaneOffset } = createPitBoxes({
332
+ laneStart,
333
+ laneForward: laneVector,
334
+ laneLength: laneVector.length,
335
+ serviceNormal,
336
+ });
337
+ const workingLaneStart = projectPitPoint(laneStart, laneVector, serviceNormal, 0, workingLaneOffset);
338
+ const workingLaneEnd = projectPitPoint(laneStart, laneVector, serviceNormal, laneVector.length, workingLaneOffset);
339
+
340
+ const pitLane = {
341
+ enabled: true,
342
+ side,
343
+ width: PIT_LANE_WIDTH,
344
+ offset: placedLaneOffset,
345
+ layout,
346
+ boxCount: PIT_BOX_COUNT,
347
+ teamCount: PIT_TEAM_COUNT,
348
+ boxesPerTeam: PIT_BOXES_PER_TEAM,
349
+ entry: {
350
+ trackDistance: entryConnection.trackDistance,
351
+ distanceFromStart: entryConnection.distanceFromStart,
352
+ trackPoint: clonePoint(entryConnection.trackPoint),
353
+ edgePoint: entryConnection.edgePoint,
354
+ trackConnectPoint: entryConnection.trackConnectPoint,
355
+ lanePoint: laneStart,
356
+ roadCenterline: entryRoadCenterline,
357
+ connector: [clonePoint(entryConnection.trackPoint), ...entryRoadCenterline, entryMergePoint],
358
+ },
359
+ exit: {
360
+ trackDistance: exitConnection.trackDistance,
361
+ distanceFromStart: exitConnection.distanceFromStart,
362
+ trackPoint: clonePoint(exitConnection.trackPoint),
363
+ edgePoint: exitConnection.edgePoint,
364
+ trackConnectPoint: exitConnection.trackConnectPoint,
365
+ lanePoint: laneEnd,
366
+ roadCenterline: exitRoadCenterline,
367
+ connector: [exitMergePoint, ...exitRoadCenterline, clonePoint(exitConnection.trackPoint)],
368
+ },
369
+ mainLane: {
370
+ start: laneStart,
371
+ end: laneEnd,
372
+ points: [laneStart, laneEnd],
373
+ length: laneVector.length,
374
+ heading: Math.atan2(laneVector.y, laneVector.x),
375
+ },
376
+ fastLane: {
377
+ offset: 0,
378
+ width: PIT_LANE_WIDTH,
379
+ },
380
+ workingLane: {
381
+ start: workingLaneStart,
382
+ end: workingLaneEnd,
383
+ points: [workingLaneStart, workingLaneEnd],
384
+ offset: workingLaneOffset,
385
+ width: PIT_WORKING_LANE_WIDTH,
386
+ },
387
+ serviceNormal,
388
+ boxes,
389
+ serviceAreas,
390
+ };
391
+ return {
392
+ ...pitLane,
393
+ bounds: createPitLaneBounds(pitLane),
394
+ };
395
+ }
@@ -0,0 +1,160 @@
1
+ import { clamp, wrapDistance } from '../simMath.js';
2
+ import { PIT_LANE_WIDTH } from './trackConstants.js';
3
+ import { nearestPointOnPolyline, pointInsideBounds, pointIsInsidePolygon } from './trackMath.js';
4
+ import { queryPitBoxCandidates, queryPitRoadSegmentCandidatesByRoute } from './trackQueryIndex.js';
5
+
6
+ export function mapPitDistance(track, startDistance, endDistance, amount) {
7
+ return wrapDistance(startDistance + (endDistance - startDistance) * clamp(amount, 0, 1), track.length);
8
+ }
9
+
10
+ export function createPitRoadState(track, position, pitLane, {
11
+ points,
12
+ surface,
13
+ part,
14
+ roadWidth,
15
+ startDistance,
16
+ endDistance,
17
+ segmentCandidates = null,
18
+ }) {
19
+ if (Array.isArray(segmentCandidates) && segmentCandidates.length === 0) return null;
20
+ const projected = nearestPointOnPolyline(points, position, segmentCandidates);
21
+ if (!projected || projected.crossTrackError > roadWidth / 2) return null;
22
+ const amount = projected.totalLength > 0 ? projected.distanceAlong / projected.totalLength : 0;
23
+ const distanceAlongTrack = mapPitDistance(track, startDistance, endDistance, amount);
24
+
25
+ return {
26
+ x: projected.point.x,
27
+ y: projected.point.y,
28
+ heading: projected.heading,
29
+ normalX: projected.normalX,
30
+ normalY: projected.normalY,
31
+ curvature: 0,
32
+ distance: distanceAlongTrack,
33
+ signedOffset: projected.signedOffset,
34
+ crossTrackError: projected.crossTrackError,
35
+ surface,
36
+ onTrack: true,
37
+ inPitLane: true,
38
+ pitLanePart: part,
39
+ pitLaneSignedOffset: projected.signedOffset,
40
+ pitLaneCrossTrackError: projected.crossTrackError,
41
+ pitLaneDistanceAlong: projected.distanceAlong,
42
+ pitLaneTotalLength: projected.totalLength,
43
+ pitLaneRouteAmount: amount,
44
+ pitLaneRoadWidth: roadWidth,
45
+ };
46
+ }
47
+
48
+ export function createPitBoxState(track, position, pitLane, candidateBoxes = null) {
49
+ const candidates = Array.isArray(candidateBoxes) ? candidateBoxes : createLegacyPitBoxCandidates(pitLane);
50
+ const serviceMatch = candidates.find((candidate) => (
51
+ (candidate.type === 'service-area' || candidate.type === 'service-queue') &&
52
+ pointIsInsidePolygon(position, candidate.polygon)
53
+ ));
54
+ const garageMatch = serviceMatch ? null : candidates.find((candidate) => (
55
+ candidate.type === 'garage-box' &&
56
+ pointIsInsidePolygon(position, candidate.polygon)
57
+ ));
58
+ const serviceArea = serviceMatch?.target ?? null;
59
+ const box = serviceArea ?? garageMatch?.target ?? null;
60
+ if (!box) return null;
61
+ const distanceAmount = pitLane.mainLane.length > 0
62
+ ? box.distanceAlongLane / pitLane.mainLane.length
63
+ : 0;
64
+ const distanceAlongTrack = mapPitDistance(
65
+ track,
66
+ pitLane.layout?.entryDistance ?? pitLane.entry.distanceFromStart,
67
+ pitLane.layout?.exitDistance ?? pitLane.exit.distanceFromStart,
68
+ distanceAmount,
69
+ );
70
+ const heading = pitLane.mainLane.heading;
71
+
72
+ return {
73
+ x: box.center.x,
74
+ y: box.center.y,
75
+ heading,
76
+ normalX: pitLane.serviceNormal.x,
77
+ normalY: pitLane.serviceNormal.y,
78
+ curvature: 0,
79
+ distance: distanceAlongTrack,
80
+ signedOffset: 0,
81
+ crossTrackError: 0,
82
+ surface: 'pit-box',
83
+ onTrack: true,
84
+ inPitLane: true,
85
+ pitLanePart: serviceArea ? 'service-box' : 'garage-box',
86
+ pitLaneSignedOffset: 0,
87
+ pitLaneCrossTrackError: 0,
88
+ pitBoxId: box.id,
89
+ pitBoxIndex: box.index,
90
+ pitTeamId: box.teamId ?? null,
91
+ pitLaneDistanceAlong: box.distanceAlongLane,
92
+ pitLaneTotalLength: pitLane.mainLane.length,
93
+ pitLaneRoadWidth: box.depth,
94
+ };
95
+ }
96
+
97
+ export function nearestPitLaneState(track, position) {
98
+ const pitLane = track.pitLane;
99
+ if (!pitLane?.enabled) return null;
100
+ if (!pointInsideBounds(position, pitLane.bounds)) return null;
101
+
102
+ const indexedBoxCandidates = queryPitBoxCandidates(track, position);
103
+ const boxCandidates = indexedBoxCandidates ?? null;
104
+ const boxState = createPitBoxState(track, position, pitLane, boxCandidates);
105
+ if (boxState) return boxState;
106
+
107
+ const laneEntryDistance = pitLane.layout?.entryDistance ?? pitLane.entry.distanceFromStart;
108
+ const laneExitDistance = pitLane.layout?.exitDistance ?? pitLane.exit.distanceFromStart;
109
+ const roadCandidatesByRoute = queryPitRoadSegmentCandidatesByRoute(track, position);
110
+ const candidates = [
111
+ createPitRoadState(track, position, pitLane, {
112
+ points: pitLane.entry.roadCenterline,
113
+ surface: 'pit-entry',
114
+ part: 'entry',
115
+ roadWidth: pitLane.width,
116
+ startDistance: pitLane.entry.distanceFromStart,
117
+ endDistance: laneEntryDistance,
118
+ segmentCandidates: roadCandidatesByRoute?.entry ?? null,
119
+ }),
120
+ createPitRoadState(track, position, pitLane, {
121
+ points: pitLane.mainLane.points,
122
+ surface: 'pit-lane',
123
+ part: 'fast-lane',
124
+ roadWidth: pitLane.width,
125
+ startDistance: laneEntryDistance,
126
+ endDistance: laneExitDistance,
127
+ segmentCandidates: roadCandidatesByRoute?.main ?? null,
128
+ }),
129
+ createPitRoadState(track, position, pitLane, {
130
+ points: pitLane.workingLane?.points,
131
+ surface: 'pit-lane',
132
+ part: 'working-lane',
133
+ roadWidth: pitLane.workingLane?.width ?? 0,
134
+ startDistance: laneEntryDistance,
135
+ endDistance: laneExitDistance,
136
+ segmentCandidates: roadCandidatesByRoute?.working ?? null,
137
+ }),
138
+ createPitRoadState(track, position, pitLane, {
139
+ points: pitLane.exit.roadCenterline,
140
+ surface: 'pit-exit',
141
+ part: 'exit',
142
+ roadWidth: pitLane.width,
143
+ startDistance: laneExitDistance,
144
+ endDistance: pitLane.exit.distanceFromStart,
145
+ segmentCandidates: roadCandidatesByRoute?.exit ?? null,
146
+ }),
147
+ ].filter(Boolean);
148
+
149
+ return candidates.sort((left, right) => left.crossTrackError - right.crossTrackError)[0] ?? null;
150
+ }
151
+
152
+ function createLegacyPitBoxCandidates(pitLane) {
153
+ return [
154
+ ...(pitLane.serviceAreas ?? []).flatMap((area) => [
155
+ { type: 'service-area', target: area, polygon: area.corners },
156
+ { type: 'service-queue', target: area, polygon: area.queueCorners },
157
+ ]),
158
+ ...(pitLane.boxes ?? []).map((box) => ({ type: 'garage-box', target: box, polygon: box.corners })),
159
+ ].filter((candidate) => Array.isArray(candidate.polygon) && candidate.polygon.length >= 3);
160
+ }