@inventure71/paddockjs 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  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 +4 -0
  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 +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -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/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,181 @@
1
+ import { buildCollisionCandidatePairs, detectVehicleCollision } from '../collisionGeometry.js';
2
+ import { canCollide, isCollidable } from '../participants/participantInteractions.js';
3
+ import { clamp, normalizeAngle } from '../simMath.js';
4
+ import { VEHICLE_LIMITS } from './vehiclePhysics.js';
5
+ import { shiftPreviousRenderPose } from '../pit/pitRouting.js';
6
+
7
+ const MAX_COLLISION_CORRECTION = 4.5;
8
+
9
+ function isPitPositionControlledCar(car) {
10
+ const status = car?.pitStop?.status;
11
+ return status === 'entering' || status === 'queued' || status === 'servicing' || status === 'exiting';
12
+ }
13
+
14
+ function forwardVector(car) {
15
+ return { x: Math.cos(car.heading), y: Math.sin(car.heading) };
16
+ }
17
+
18
+ function velocityVector(car) {
19
+ if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
20
+ return { x: car.velocityX, y: car.velocityY };
21
+ }
22
+ const forward = forwardVector(car);
23
+ return { x: forward.x * car.speed, y: forward.y * car.speed };
24
+ }
25
+
26
+ function dot(a, b) {
27
+ return a.x * b.x + a.y * b.y;
28
+ }
29
+
30
+ function applyVelocityVector(car, velocity) {
31
+ const speed = clamp(Math.hypot(velocity.x, velocity.y), 0, VEHICLE_LIMITS.maxSpeed);
32
+ if (speed <= 0) {
33
+ car.velocityX = 0;
34
+ car.velocityY = 0;
35
+ car.speed = 0;
36
+ return;
37
+ }
38
+ const scale = speed / Math.max(1e-9, Math.hypot(velocity.x, velocity.y));
39
+ car.velocityX = velocity.x * scale;
40
+ car.velocityY = velocity.y * scale;
41
+ car.speed = speed;
42
+ }
43
+
44
+ export function resolveCollisionsForSimulation(sim) {
45
+ const collidableCars = sim.cars.filter(isCollidable);
46
+ if (collidableCars.length < 2) return;
47
+ const reportedContacts = new Set();
48
+
49
+ for (let pass = 0; pass < 3; pass += 1) {
50
+ const candidates = buildCollisionCandidatePairs(collidableCars, { trackLength: sim.track.length });
51
+ for (const [first, second] of candidates) {
52
+ if (!canCollide(first, second)) continue;
53
+ const collision = detectVehicleCollision(first, second);
54
+ if (!collision) continue;
55
+ const firstPitControlled = isPitPositionControlledCar(first);
56
+ const secondPitControlled = isPitPositionControlledCar(second);
57
+ if (firstPitControlled && secondPitControlled) continue;
58
+ const stewardCollision = {
59
+ ...collision,
60
+ trackLength: sim.track.length,
61
+ };
62
+ const oneCarFixed = firstPitControlled || secondPitControlled;
63
+
64
+ const correction = Math.min(
65
+ (oneCarFixed ? collision.depth : collision.depth / 2) + 0.65,
66
+ MAX_COLLISION_CORRECTION,
67
+ );
68
+ const firstCorrectionX = firstPitControlled ? 0 : -collision.axis.x * correction;
69
+ const firstCorrectionY = firstPitControlled ? 0 : -collision.axis.y * correction;
70
+ const secondCorrectionX = secondPitControlled ? 0 : collision.axis.x * correction;
71
+ const secondCorrectionY = secondPitControlled ? 0 : collision.axis.y * correction;
72
+ if (!firstPitControlled) {
73
+ first.x += firstCorrectionX;
74
+ first.y += firstCorrectionY;
75
+ }
76
+ if (!secondPitControlled) {
77
+ second.x += secondCorrectionX;
78
+ second.y += secondCorrectionY;
79
+ }
80
+
81
+ if (oneCarFixed) {
82
+ if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode === 'simulator');
83
+ if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode === 'simulator');
84
+ } else {
85
+ applyContactVelocityResponse(sim, first, second, collision.axis);
86
+ }
87
+
88
+ const yawNudge = clamp(collision.depth * 0.0025, 0.008, 0.035);
89
+ const freshContact = first.contactCooldown <= 0 && second.contactCooldown <= 0;
90
+ const firstHeadingCorrection = firstPitControlled ? 0 : -collision.axis.y * yawNudge;
91
+ const secondHeadingCorrection = secondPitControlled ? 0 : collision.axis.y * yawNudge;
92
+ if (!firstPitControlled) {
93
+ first.heading = normalizeAngle(first.heading + firstHeadingCorrection);
94
+ shiftPreviousRenderPose(first, firstCorrectionX, firstCorrectionY, firstHeadingCorrection);
95
+ }
96
+ if (!secondPitControlled) {
97
+ second.heading = normalizeAngle(second.heading + secondHeadingCorrection);
98
+ shiftPreviousRenderPose(second, secondCorrectionX, secondCorrectionY, secondHeadingCorrection);
99
+ }
100
+ first.contactCooldown = 1;
101
+ second.contactCooldown = 1;
102
+
103
+ const contactKey = `${first.id}:${second.id}`;
104
+ if (freshContact && pass === 0 && !reportedContacts.has(contactKey)) {
105
+ reportedContacts.add(contactKey);
106
+ sim.events.unshift({
107
+ type: 'contact',
108
+ at: sim.time,
109
+ carId: first.id,
110
+ otherCarId: second.id,
111
+ firstShapeId: collision.firstShapeId,
112
+ secondShapeId: collision.secondShapeId,
113
+ contactType: collision.contactType,
114
+ depth: collision.depth,
115
+ timeOfImpact: collision.timeOfImpact,
116
+ });
117
+ sim.reviewCollision(first, second, stewardCollision);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ export function applyContactVelocityResponse(sim, first, second, axis) {
124
+ if (
125
+ sim.physicsMode === 'simulator' ||
126
+ Number.isFinite(first.velocityX) ||
127
+ Number.isFinite(second.velocityX)
128
+ ) {
129
+ const firstVelocity = velocityVector(first);
130
+ const secondVelocity = velocityVector(second);
131
+ const relativeNormalVelocity = dot({
132
+ x: secondVelocity.x - firstVelocity.x,
133
+ y: secondVelocity.y - firstVelocity.y,
134
+ }, axis);
135
+
136
+ if (relativeNormalVelocity < 0) {
137
+ const impulse = clamp(-relativeNormalVelocity * (0.34 + sim.rules.collisionRestitution), 0, 16);
138
+ firstVelocity.x -= axis.x * impulse;
139
+ firstVelocity.y -= axis.y * impulse;
140
+ secondVelocity.x += axis.x * impulse;
141
+ secondVelocity.y += axis.y * impulse;
142
+ }
143
+
144
+ applyVelocityVector(first, {
145
+ x: firstVelocity.x * 0.997,
146
+ y: firstVelocity.y * 0.997,
147
+ });
148
+ applyVelocityVector(second, {
149
+ x: secondVelocity.x * 0.997,
150
+ y: secondVelocity.y * 0.997,
151
+ });
152
+ return;
153
+ }
154
+
155
+ const firstForward = forwardVector(first);
156
+ const secondForward = forwardVector(second);
157
+ const firstNormal = dot(firstForward, axis);
158
+ const secondNormal = dot(secondForward, axis);
159
+ const relativeNormalVelocity = second.speed * secondNormal - first.speed * firstNormal;
160
+
161
+ if (relativeNormalVelocity < 0) {
162
+ const impulse = clamp(-relativeNormalVelocity * (0.34 + sim.rules.collisionRestitution), 0, 16);
163
+ if (firstNormal > 0) first.speed = clamp(first.speed - impulse * firstNormal, 0, VEHICLE_LIMITS.maxSpeed);
164
+ if (secondNormal < 0) second.speed = clamp(second.speed + impulse * secondNormal, 0, VEHICLE_LIMITS.maxSpeed);
165
+ }
166
+
167
+ first.speed = clamp(first.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
168
+ second.speed = clamp(second.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
169
+ }
170
+
171
+ function dampPitContactVelocity(car, factor, syncVelocity = false) {
172
+ if (syncVelocity || Number.isFinite(car.velocityX) || Number.isFinite(car.velocityY)) {
173
+ const velocity = velocityVector(car);
174
+ applyVelocityVector(car, {
175
+ x: velocity.x * factor,
176
+ y: velocity.y * factor,
177
+ });
178
+ return;
179
+ }
180
+ car.speed = clamp(car.speed * factor, 0, VEHICLE_LIMITS.maxSpeed);
181
+ }
@@ -0,0 +1,82 @@
1
+ export function mainTrackSurface(track, crossTrackError) {
2
+ const trackEdge = track.width / 2;
3
+ const kerbEdge = trackEdge + (track.kerbWidth ?? 0);
4
+ const gravelEdge = kerbEdge + track.gravelWidth;
5
+ const runoffEdge = gravelEdge + track.runoffWidth;
6
+ if (crossTrackError <= trackEdge) return 'track';
7
+ if (crossTrackError <= kerbEdge) return 'kerb';
8
+ if (crossTrackError <= gravelEdge) return 'gravel';
9
+ if (crossTrackError <= runoffEdge) return 'grass';
10
+ return 'barrier';
11
+ }
12
+
13
+ export function wheelFullyOutside(samples, trackLimit) {
14
+ const rightOutside = samples.every((state) => !state.inPitLane && state.signedOffset > trackLimit);
15
+ const leftOutside = samples.every((state) => !state.inPitLane && state.signedOffset < -trackLimit);
16
+ return {
17
+ rightOutside,
18
+ leftOutside,
19
+ fullyOutsideWhiteLine: rightOutside || leftOutside,
20
+ outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
21
+ };
22
+ }
23
+
24
+ export function wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit) {
25
+ const rightOutside = minimumSignedOffset > trackLimit;
26
+ const leftOutside = maximumSignedOffset < -trackLimit;
27
+ return {
28
+ rightOutside,
29
+ leftOutside,
30
+ fullyOutsideWhiteLine: rightOutside || leftOutside,
31
+ outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
32
+ };
33
+ }
34
+
35
+ export function analyticWheelState(patch, centerState, track, trackLimit) {
36
+ const wheelCenterOffset =
37
+ (patch.center.x - centerState.x) * centerState.normalX +
38
+ (patch.center.y - centerState.y) * centerState.normalY;
39
+ const projectedHalfWidth =
40
+ Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
41
+ Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
42
+ const minimumSignedOffset = wheelCenterOffset - projectedHalfWidth;
43
+ const maximumSignedOffset = wheelCenterOffset + projectedHalfWidth;
44
+ const signedOffset = Math.abs(minimumSignedOffset) > Math.abs(maximumSignedOffset)
45
+ ? minimumSignedOffset
46
+ : maximumSignedOffset;
47
+ const state = stateFromSignedOffset(centerState, track, signedOffset);
48
+ const outside = wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit);
49
+
50
+ return {
51
+ id: patch.id,
52
+ x: patch.center.x,
53
+ y: patch.center.y,
54
+ signedOffset: state.signedOffset,
55
+ crossTrackError: state.crossTrackError,
56
+ surface: state.surface,
57
+ onTrack: Boolean(state.onTrack),
58
+ inPitLane: false,
59
+ pitLanePart: null,
60
+ pitBoxId: null,
61
+ minimumSignedOffset,
62
+ maximumSignedOffset,
63
+ fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
64
+ outsideSide: outside.outsideSide,
65
+ sampledStates: [state],
66
+ };
67
+ }
68
+
69
+ function stateFromSignedOffset(centerState, track, signedOffset) {
70
+ const crossTrackError = Math.abs(signedOffset);
71
+ const surface = mainTrackSurface(track, crossTrackError);
72
+ return {
73
+ ...centerState,
74
+ signedOffset,
75
+ crossTrackError,
76
+ surface,
77
+ onTrack: surface === 'track' || surface === 'kerb',
78
+ inPitLane: false,
79
+ pitLanePart: null,
80
+ pitBoxId: null,
81
+ };
82
+ }
@@ -0,0 +1,69 @@
1
+ import { metersToSimUnits } from '../units.js';
2
+
3
+ const PIT_CONNECTOR_FULL_SAMPLE_WINDOW = metersToSimUnits(35);
4
+
5
+ export function patchSamples(patch) {
6
+ return [patch.center, ...patch.corners];
7
+ }
8
+
9
+ export function isNearPitConnector(track, centerState) {
10
+ const pitLane = track.pitLane;
11
+ if (!pitLane?.enabled) return false;
12
+ const distance = centerState.distance;
13
+ const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
14
+ const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
15
+ return wrappedDistanceDelta(distance, entryDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW ||
16
+ wrappedDistanceDelta(distance, exitDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW;
17
+ }
18
+
19
+ export function canUseAnalyticPitWheels(geometry, centerState) {
20
+ const roadWidth = Number(centerState?.pitLaneRoadWidth);
21
+ if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
22
+ const halfRoadWidth = roadWidth / 2;
23
+ return geometry.contactPatches.every((patch) => {
24
+ const range = patchPitOffsetRange(patch, centerState);
25
+ return range.minimum >= -halfRoadWidth - 0.001 && range.maximum <= halfRoadWidth + 0.001;
26
+ });
27
+ }
28
+
29
+ export function analyticPitWheelState(patch, centerState) {
30
+ const range = patchPitOffsetRange(patch, centerState);
31
+ return {
32
+ id: patch.id,
33
+ x: patch.center.x,
34
+ y: patch.center.y,
35
+ signedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
36
+ crossTrackError: centerState.mainTrackCrossTrackError ?? centerState.crossTrackError,
37
+ surface: centerState.surface,
38
+ onTrack: true,
39
+ inPitLane: true,
40
+ pitLanePart: centerState.pitLanePart ?? null,
41
+ pitBoxId: centerState.pitBoxId ?? null,
42
+ minimumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
43
+ maximumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
44
+ pitLaneMinimumSignedOffset: range.minimum,
45
+ pitLaneMaximumSignedOffset: range.maximum,
46
+ fullyOutsideWhiteLine: false,
47
+ outsideSide: 0,
48
+ sampledStates: [centerState],
49
+ };
50
+ }
51
+
52
+ function patchPitOffsetRange(patch, centerState) {
53
+ const wheelCenterOffset =
54
+ (patch.center.x - centerState.x) * centerState.normalX +
55
+ (patch.center.y - centerState.y) * centerState.normalY;
56
+ const projectedHalfWidth =
57
+ Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
58
+ Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
59
+ return {
60
+ minimum: wheelCenterOffset - projectedHalfWidth,
61
+ maximum: wheelCenterOffset + projectedHalfWidth,
62
+ };
63
+ }
64
+
65
+ function wrappedDistanceDelta(first, second, length) {
66
+ if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(length) || length <= 0) return Infinity;
67
+ const delta = Math.abs(first - second);
68
+ return Math.min(delta, length - delta);
69
+ }
@@ -0,0 +1,87 @@
1
+ import { VEHICLE_GEOMETRY, getVehicleGeometryState, vehicleAxes } from './vehicleGeometry.js';
2
+ import { applyWheelSurfaceState } from './wheelSurface.js';
3
+ import { freezeVehicleMotion } from './vehicleKinematics.js';
4
+ import { nearestTrackStateForCar, pitOverrideAllowedForCar } from '../track/trackStatePolicy.js';
5
+ import { markCarDnf } from '../race/retirements.js';
6
+
7
+ export function applyRunoffResponseForSimulation(sim, car) {
8
+ if (car.destroyed) {
9
+ freezeBarrierMotion(car, sim.physicsMode);
10
+ applyWheelSurfaceState(car, sim.track);
11
+ return;
12
+ }
13
+ const allowPitOverride = pitOverrideAllowedForCar(car);
14
+ const state = nearestTrackStateForCar(sim.track, car, car, car.progress, { allowPitOverride });
15
+ const mainTrackState = state.inPitLane
16
+ ? nearestTrackStateForCar(sim.track, car, car, car.progress, { allowPitOverride: false })
17
+ : state;
18
+ const barrierCenter = sim.track.width / 2 + (sim.track.kerbWidth ?? 0) + sim.track.gravelWidth + sim.track.runoffWidth;
19
+ const signedLimit = barrierCenter - (sim.track.barrierWidth ?? 0) / 2;
20
+ const side = Math.sign(mainTrackState.signedOffset) || 1;
21
+ const outwardReach = getOutwardVehicleReach(car, mainTrackState, side);
22
+ const overshoot = Math.abs(mainTrackState.signedOffset) + outwardReach - signedLimit;
23
+ if (overshoot <= 0) {
24
+ if (state.inPitLane) {
25
+ applyWheelSurfaceState(car, sim.track, { centerState: state });
26
+ return;
27
+ }
28
+ applyWheelSurfaceState(car, sim.track, { centerState: state });
29
+ return;
30
+ }
31
+
32
+ destroyCarOnBarrier(sim, car);
33
+ applyWheelSurfaceState(car, sim.track, { centerState: mainTrackState });
34
+ }
35
+
36
+ function getOutwardVehicleReach(car, state, side) {
37
+ const visualReach = getVisualFootprintOutwardReach(car, state);
38
+ const geometry = getVehicleGeometryState(car);
39
+ let reach = visualReach;
40
+ geometry.current.shapes.forEach((shape) => {
41
+ shape.corners.forEach((corner) => {
42
+ const lateral =
43
+ (corner.x - car.x) * state.normalX +
44
+ (corner.y - car.y) * state.normalY;
45
+ reach = Math.max(reach, lateral * side);
46
+ });
47
+ });
48
+ return reach;
49
+ }
50
+
51
+ function getVisualFootprintOutwardReach(car, state) {
52
+ const axes = vehicleAxes(car.heading ?? 0);
53
+ return (
54
+ Math.abs(axes.forward.x * state.normalX + axes.forward.y * state.normalY) * VEHICLE_GEOMETRY.visualLength / 2 +
55
+ Math.abs(axes.right.x * state.normalX + axes.right.y * state.normalY) * VEHICLE_GEOMETRY.visualWidth / 2
56
+ );
57
+ }
58
+
59
+ function destroyCarOnBarrier(sim, car) {
60
+ if (car.destroyed || car.finished) return;
61
+ car.destroyed = true;
62
+ car.destroyReason = 'barrier';
63
+ car.destroyedAt = sim.time;
64
+ car.outOfRace = true;
65
+ markCarDnf(sim, car, { reason: 'barrier' });
66
+ car.canAttack = false;
67
+ car.drsActive = false;
68
+ car.drsEligible = false;
69
+ car.drsZoneId = null;
70
+ car.drsZoneEnabled = false;
71
+ freezeBarrierMotion(car, sim.physicsMode);
72
+ sim.events.unshift({
73
+ type: 'car-destroyed',
74
+ at: sim.time,
75
+ carId: car.id,
76
+ driverId: car.id,
77
+ reason: 'barrier',
78
+ });
79
+ }
80
+
81
+ function freezeBarrierMotion(car, physicsMode) {
82
+ freezeVehicleMotion(car, {
83
+ clearManualControls: true,
84
+ physicsMode,
85
+ stabilityState: 'destroyed',
86
+ });
87
+ }
@@ -0,0 +1,28 @@
1
+ const SURFACE_PRIORITY = {
2
+ track: 0,
3
+ 'pit-entry': 1,
4
+ 'pit-exit': 1,
5
+ 'pit-lane': 2,
6
+ 'pit-box': 3,
7
+ kerb: 4,
8
+ grass: 5,
9
+ gravel: 6,
10
+ barrier: 7,
11
+ };
12
+
13
+ export function priority(surface) {
14
+ return SURFACE_PRIORITY[surface] ?? SURFACE_PRIORITY.barrier;
15
+ }
16
+
17
+ export function worstState(states) {
18
+ return states.reduce((worst, state) => (
19
+ priority(state.surface) > priority(worst.surface) ? state : worst
20
+ ), states[0]);
21
+ }
22
+
23
+ export function getEffectiveSurface(wheels = []) {
24
+ if (!wheels.length) return 'track';
25
+ return wheels.reduce((surface, wheel) => (
26
+ priority(wheel.surface) > priority(surface) ? wheel.surface : surface
27
+ ), 'track');
28
+ }
@@ -0,0 +1,247 @@
1
+ import { normalizeAngle } from '../simMath.js';
2
+ import {
3
+ REAL_F1_CAR_LENGTH_METERS,
4
+ REAL_F1_CAR_WIDTH_METERS,
5
+ REAL_F1_WHEELBASE_METERS,
6
+ metersToSimUnits,
7
+ } from '../units.js';
8
+
9
+ export const VEHICLE_GEOMETRY = {
10
+ visualLength: metersToSimUnits(REAL_F1_CAR_LENGTH_METERS),
11
+ visualWidth: metersToSimUnits(REAL_F1_CAR_WIDTH_METERS),
12
+ bodyLength: metersToSimUnits(4.65),
13
+ bodyWidth: metersToSimUnits(1.24),
14
+ wheelLength: metersToSimUnits(0.72),
15
+ wheelWidth: metersToSimUnits(0.38),
16
+ wheelLongitudinalOffset: metersToSimUnits(REAL_F1_WHEELBASE_METERS / 2),
17
+ wheelLateralOffset: metersToSimUnits((REAL_F1_CAR_WIDTH_METERS - 0.38) / 2),
18
+ };
19
+
20
+ const WHEEL_SPECS = [
21
+ { id: 'front-left', longitudinal: 1, lateral: -1 },
22
+ { id: 'front-right', longitudinal: 1, lateral: 1 },
23
+ { id: 'rear-left', longitudinal: -1, lateral: -1 },
24
+ { id: 'rear-right', longitudinal: -1, lateral: 1 },
25
+ ];
26
+
27
+ function finiteOr(value, fallback) {
28
+ return Number.isFinite(value) ? value : fallback;
29
+ }
30
+
31
+ function vehiclePose(car, { previous = false } = {}) {
32
+ const x = previous ? finiteOr(car.previousX, car.x) : car.x;
33
+ const y = previous ? finiteOr(car.previousY, car.y) : car.y;
34
+ const heading = previous ? finiteOr(car.previousHeading, car.heading) : car.heading;
35
+
36
+ return {
37
+ x: finiteOr(x, 0),
38
+ y: finiteOr(y, 0),
39
+ heading: normalizeAngle(finiteOr(heading, 0)),
40
+ };
41
+ }
42
+
43
+ export function vehicleAxes(heading) {
44
+ const cos = Math.cos(heading);
45
+ const sin = Math.sin(heading);
46
+ return {
47
+ forward: { x: cos, y: sin },
48
+ right: { x: -sin, y: cos },
49
+ };
50
+ }
51
+
52
+ function offsetPoint(center, axes, longitudinal, lateral) {
53
+ return {
54
+ x: center.x + axes.forward.x * longitudinal + axes.right.x * lateral,
55
+ y: center.y + axes.forward.y * longitudinal + axes.right.y * lateral,
56
+ };
57
+ }
58
+
59
+ export function createOrientedRect({ id, type, center, heading, length, width }) {
60
+ const axes = vehicleAxes(heading);
61
+ const halfLength = length / 2;
62
+ const halfWidth = width / 2;
63
+ const corners = [
64
+ offsetPoint(center, axes, halfLength, halfWidth),
65
+ offsetPoint(center, axes, halfLength, -halfWidth),
66
+ offsetPoint(center, axes, -halfLength, -halfWidth),
67
+ offsetPoint(center, axes, -halfLength, halfWidth),
68
+ ];
69
+
70
+ return {
71
+ id,
72
+ type,
73
+ center,
74
+ heading,
75
+ length,
76
+ width,
77
+ halfLength,
78
+ halfWidth,
79
+ forward: axes.forward,
80
+ right: axes.right,
81
+ corners,
82
+ };
83
+ }
84
+
85
+ export function createVehicleGeometry(car, options = {}) {
86
+ const pose = vehiclePose(car, options);
87
+ const axes = vehicleAxes(pose.heading);
88
+ const center = { x: pose.x, y: pose.y };
89
+ const body = createOrientedRect({
90
+ id: 'body',
91
+ type: 'body',
92
+ center,
93
+ heading: pose.heading,
94
+ length: VEHICLE_GEOMETRY.bodyLength,
95
+ width: VEHICLE_GEOMETRY.bodyWidth,
96
+ });
97
+ const wheels = WHEEL_SPECS.map((spec) => {
98
+ const wheelCenter = offsetPoint(
99
+ center,
100
+ axes,
101
+ spec.longitudinal * VEHICLE_GEOMETRY.wheelLongitudinalOffset,
102
+ spec.lateral * VEHICLE_GEOMETRY.wheelLateralOffset,
103
+ );
104
+ return createOrientedRect({
105
+ id: spec.id,
106
+ type: 'wheel',
107
+ center: wheelCenter,
108
+ heading: pose.heading,
109
+ length: VEHICLE_GEOMETRY.wheelLength,
110
+ width: VEHICLE_GEOMETRY.wheelWidth,
111
+ });
112
+ });
113
+
114
+ return {
115
+ body,
116
+ wheels,
117
+ contactPatches: wheels,
118
+ shapes: [body, ...wheels],
119
+ };
120
+ }
121
+
122
+ function geometryPoseSignature(pose) {
123
+ return [
124
+ pose.x,
125
+ pose.y,
126
+ pose.heading,
127
+ pose.previousX,
128
+ pose.previousY,
129
+ pose.previousHeading,
130
+ ].join(':');
131
+ }
132
+
133
+ function geometryPose(car) {
134
+ const x = finiteOr(car.x, 0);
135
+ const y = finiteOr(car.y, 0);
136
+ const heading = normalizeAngle(finiteOr(car.heading, 0));
137
+ return {
138
+ x,
139
+ y,
140
+ heading,
141
+ previousX: finiteOr(car.previousX, x),
142
+ previousY: finiteOr(car.previousY, y),
143
+ previousHeading: normalizeAngle(finiteOr(car.previousHeading, heading)),
144
+ };
145
+ }
146
+
147
+ function geometryStateMatches(car, state) {
148
+ const pose = state?.pose;
149
+ if (!pose) return false;
150
+ const x = finiteOr(car.x, 0);
151
+ const y = finiteOr(car.y, 0);
152
+ const heading = normalizeAngle(finiteOr(car.heading, 0));
153
+ return (
154
+ pose.x === x &&
155
+ pose.y === y &&
156
+ pose.heading === heading &&
157
+ pose.previousX === finiteOr(car.previousX, x) &&
158
+ pose.previousY === finiteOr(car.previousY, y) &&
159
+ pose.previousHeading === normalizeAngle(finiteOr(car.previousHeading, heading))
160
+ );
161
+ }
162
+
163
+ export function createVehicleShapeAabb(shape) {
164
+ let minX = Infinity;
165
+ let maxX = -Infinity;
166
+ let minY = Infinity;
167
+ let maxY = -Infinity;
168
+ for (let index = 0; index < shape.corners.length; index += 1) {
169
+ const corner = shape.corners[index];
170
+ if (corner.x < minX) minX = corner.x;
171
+ if (corner.x > maxX) maxX = corner.x;
172
+ if (corner.y < minY) minY = corner.y;
173
+ if (corner.y > maxY) maxY = corner.y;
174
+ }
175
+ return {
176
+ minX,
177
+ maxX,
178
+ minY,
179
+ maxY,
180
+ };
181
+ }
182
+
183
+ export function mergeAabbs(aabbs) {
184
+ let minX = Infinity;
185
+ let maxX = -Infinity;
186
+ let minY = Infinity;
187
+ let maxY = -Infinity;
188
+ for (let index = 0; index < aabbs.length; index += 1) {
189
+ const aabb = aabbs[index];
190
+ if (aabb.minX < minX) minX = aabb.minX;
191
+ if (aabb.maxX > maxX) maxX = aabb.maxX;
192
+ if (aabb.minY < minY) minY = aabb.minY;
193
+ if (aabb.maxY > maxY) maxY = aabb.maxY;
194
+ }
195
+ return { minX, maxX, minY, maxY };
196
+ }
197
+
198
+ export function createVehicleGeometryState(car) {
199
+ const pose = geometryPose(car);
200
+ const current = createVehicleGeometry(car);
201
+ const previous = createVehicleGeometry(car, { previous: true });
202
+ const bodyAabb = createVehicleShapeAabb(current.body);
203
+ const previousBodyAabb = createVehicleShapeAabb(previous.body);
204
+ const sweptBodyAabb = mergeAabbs([previousBodyAabb, bodyAabb]);
205
+
206
+ return {
207
+ pose,
208
+ signature: geometryPoseSignature(pose),
209
+ current,
210
+ previous,
211
+ body: current.body,
212
+ wheels: current.wheels,
213
+ contactPatches: current.contactPatches,
214
+ bodyAabb,
215
+ previousBodyAabb,
216
+ sweptBodyAabb,
217
+ };
218
+ }
219
+
220
+ export function getVehicleGeometryState(car) {
221
+ if (geometryStateMatches(car, car.geometryState)) return car.geometryState;
222
+ car.geometryState = createVehicleGeometryState(car);
223
+ return car.geometryState;
224
+ }
225
+
226
+ export function createVehicleAabb(car, options = {}) {
227
+ const geometry = createVehicleGeometry(car, options);
228
+ return mergeAabbs(geometry.shapes.map(createVehicleShapeAabb));
229
+ }
230
+
231
+ export function interpolateVehiclePose(car, amount) {
232
+ const previousX = finiteOr(car.previousX, car.x);
233
+ const previousY = finiteOr(car.previousY, car.y);
234
+ const previousHeading = finiteOr(car.previousHeading, car.heading);
235
+ const headingDelta = normalizeAngle(finiteOr(car.heading, 0) - previousHeading);
236
+
237
+ return {
238
+ ...car,
239
+ x: previousX + (finiteOr(car.x, previousX) - previousX) * amount,
240
+ y: previousY + (finiteOr(car.y, previousY) - previousY) * amount,
241
+ heading: normalizeAngle(previousHeading + headingDelta * amount),
242
+ };
243
+ }
244
+
245
+ export function getCarCorners(car) {
246
+ return createVehicleGeometry(car).body.corners;
247
+ }