@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,116 @@
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 dot(a, b) {
19
+ return a.x * b.x + a.y * b.y;
20
+ }
21
+
22
+ export function resolveCollisionsForSimulation(sim) {
23
+ const collidableCars = sim.cars.filter(isCollidable);
24
+ if (collidableCars.length < 2) return;
25
+ const reportedContacts = new Set();
26
+
27
+ for (let pass = 0; pass < 3; pass += 1) {
28
+ const candidates = buildCollisionCandidatePairs(collidableCars, { trackLength: sim.track.length });
29
+ for (const [first, second] of candidates) {
30
+ if (!canCollide(first, second)) continue;
31
+ const collision = detectVehicleCollision(first, second);
32
+ if (!collision) continue;
33
+ const firstPitControlled = isPitPositionControlledCar(first);
34
+ const secondPitControlled = isPitPositionControlledCar(second);
35
+ if (firstPitControlled && secondPitControlled) continue;
36
+ const stewardCollision = {
37
+ ...collision,
38
+ trackLength: sim.track.length,
39
+ };
40
+ const oneCarFixed = firstPitControlled || secondPitControlled;
41
+
42
+ const correction = Math.min(
43
+ (oneCarFixed ? collision.depth : collision.depth / 2) + 0.65,
44
+ MAX_COLLISION_CORRECTION,
45
+ );
46
+ const firstCorrectionX = firstPitControlled ? 0 : -collision.axis.x * correction;
47
+ const firstCorrectionY = firstPitControlled ? 0 : -collision.axis.y * correction;
48
+ const secondCorrectionX = secondPitControlled ? 0 : collision.axis.x * correction;
49
+ const secondCorrectionY = secondPitControlled ? 0 : collision.axis.y * correction;
50
+ if (!firstPitControlled) {
51
+ first.x += firstCorrectionX;
52
+ first.y += firstCorrectionY;
53
+ }
54
+ if (!secondPitControlled) {
55
+ second.x += secondCorrectionX;
56
+ second.y += secondCorrectionY;
57
+ }
58
+
59
+ if (oneCarFixed) {
60
+ if (!firstPitControlled) first.speed = clamp(first.speed * 0.985, 0, VEHICLE_LIMITS.maxSpeed);
61
+ if (!secondPitControlled) second.speed = clamp(second.speed * 0.985, 0, VEHICLE_LIMITS.maxSpeed);
62
+ } else {
63
+ applyContactVelocityResponse(sim, first, second, collision.axis);
64
+ }
65
+
66
+ const yawNudge = clamp(collision.depth * 0.0025, 0.008, 0.035);
67
+ const freshContact = first.contactCooldown <= 0 && second.contactCooldown <= 0;
68
+ const firstHeadingCorrection = firstPitControlled ? 0 : -collision.axis.y * yawNudge;
69
+ const secondHeadingCorrection = secondPitControlled ? 0 : collision.axis.y * yawNudge;
70
+ if (!firstPitControlled) {
71
+ first.heading = normalizeAngle(first.heading + firstHeadingCorrection);
72
+ shiftPreviousRenderPose(first, firstCorrectionX, firstCorrectionY, firstHeadingCorrection);
73
+ }
74
+ if (!secondPitControlled) {
75
+ second.heading = normalizeAngle(second.heading + secondHeadingCorrection);
76
+ shiftPreviousRenderPose(second, secondCorrectionX, secondCorrectionY, secondHeadingCorrection);
77
+ }
78
+ first.contactCooldown = 1;
79
+ second.contactCooldown = 1;
80
+
81
+ const contactKey = `${first.id}:${second.id}`;
82
+ if (freshContact && pass === 0 && !reportedContacts.has(contactKey)) {
83
+ reportedContacts.add(contactKey);
84
+ sim.events.unshift({
85
+ type: 'contact',
86
+ at: sim.time,
87
+ carId: first.id,
88
+ otherCarId: second.id,
89
+ firstShapeId: collision.firstShapeId,
90
+ secondShapeId: collision.secondShapeId,
91
+ contactType: collision.contactType,
92
+ depth: collision.depth,
93
+ timeOfImpact: collision.timeOfImpact,
94
+ });
95
+ sim.reviewCollision(first, second, stewardCollision);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ export function applyContactVelocityResponse(sim, first, second, axis) {
102
+ const firstForward = forwardVector(first);
103
+ const secondForward = forwardVector(second);
104
+ const firstNormal = dot(firstForward, axis);
105
+ const secondNormal = dot(secondForward, axis);
106
+ const relativeNormalVelocity = second.speed * secondNormal - first.speed * firstNormal;
107
+
108
+ if (relativeNormalVelocity < 0) {
109
+ const impulse = clamp(-relativeNormalVelocity * (0.34 + sim.rules.collisionRestitution), 0, 16);
110
+ if (firstNormal > 0) first.speed = clamp(first.speed - impulse * firstNormal, 0, VEHICLE_LIMITS.maxSpeed);
111
+ if (secondNormal < 0) second.speed = clamp(second.speed + impulse * secondNormal, 0, VEHICLE_LIMITS.maxSpeed);
112
+ }
113
+
114
+ first.speed = clamp(first.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
115
+ second.speed = clamp(second.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
116
+ }
@@ -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,88 @@
1
+ import { VEHICLE_GEOMETRY, getVehicleGeometryState, vehicleAxes } from './vehicleGeometry.js';
2
+ import { applyWheelSurfaceState } from './wheelSurface.js';
3
+ import { nearestTrackStateForCar, pitOverrideAllowedForCar } from '../track/trackStatePolicy.js';
4
+ import { markCarDnf } from '../race/retirements.js';
5
+
6
+ export function applyRunoffResponseForSimulation(sim, car) {
7
+ if (car.destroyed) {
8
+ car.speed = 0;
9
+ applyWheelSurfaceState(car, sim.track);
10
+ return;
11
+ }
12
+ const allowPitOverride = pitOverrideAllowedForCar(car);
13
+ const state = nearestTrackStateForCar(sim.track, car, car, car.progress, { allowPitOverride });
14
+ const mainTrackState = state.inPitLane
15
+ ? nearestTrackStateForCar(sim.track, car, car, car.progress, { allowPitOverride: false })
16
+ : state;
17
+ const barrierCenter = sim.track.width / 2 + (sim.track.kerbWidth ?? 0) + sim.track.gravelWidth + sim.track.runoffWidth;
18
+ const signedLimit = barrierCenter - (sim.track.barrierWidth ?? 0) / 2;
19
+ const side = Math.sign(mainTrackState.signedOffset) || 1;
20
+ const outwardReach = getOutwardVehicleReach(car, mainTrackState, side);
21
+ const overshoot = Math.abs(mainTrackState.signedOffset) + outwardReach - signedLimit;
22
+ if (overshoot <= 0) {
23
+ if (state.inPitLane) {
24
+ applyWheelSurfaceState(car, sim.track, { centerState: state });
25
+ return;
26
+ }
27
+ applyWheelSurfaceState(car, sim.track, { centerState: state });
28
+ return;
29
+ }
30
+
31
+ destroyCarOnBarrier(sim, car);
32
+ applyWheelSurfaceState(car, sim.track, { centerState: mainTrackState });
33
+ }
34
+
35
+ function getOutwardVehicleReach(car, state, side) {
36
+ const visualReach = getVisualFootprintOutwardReach(car, state);
37
+ const geometry = getVehicleGeometryState(car);
38
+ let reach = visualReach;
39
+ geometry.current.shapes.forEach((shape) => {
40
+ shape.corners.forEach((corner) => {
41
+ const lateral =
42
+ (corner.x - car.x) * state.normalX +
43
+ (corner.y - car.y) * state.normalY;
44
+ reach = Math.max(reach, lateral * side);
45
+ });
46
+ });
47
+ return reach;
48
+ }
49
+
50
+ function getVisualFootprintOutwardReach(car, state) {
51
+ const axes = vehicleAxes(car.heading ?? 0);
52
+ return (
53
+ Math.abs(axes.forward.x * state.normalX + axes.forward.y * state.normalY) * VEHICLE_GEOMETRY.visualLength / 2 +
54
+ Math.abs(axes.right.x * state.normalX + axes.right.y * state.normalY) * VEHICLE_GEOMETRY.visualWidth / 2
55
+ );
56
+ }
57
+
58
+ function destroyCarOnBarrier(sim, car) {
59
+ if (car.destroyed || car.finished) return;
60
+ car.destroyed = true;
61
+ car.destroyReason = 'barrier';
62
+ car.destroyedAt = sim.time;
63
+ car.outOfRace = true;
64
+ markCarDnf(sim, car, { reason: 'barrier' });
65
+ car.canAttack = false;
66
+ car.drsActive = false;
67
+ car.drsEligible = false;
68
+ car.drsZoneId = null;
69
+ car.drsZoneEnabled = false;
70
+ car.speed = 0;
71
+ car.throttle = 0;
72
+ car.brake = 1;
73
+ car.steeringAngle = 0;
74
+ car.yawRate = 0;
75
+ car.longitudinalAcceleration = 0;
76
+ car.lateralAcceleration = 0;
77
+ car.longitudinalG = 0;
78
+ car.lateralG = 0;
79
+ car.tractionLimited = false;
80
+ car.stabilityState = 'destroyed';
81
+ sim.events.unshift({
82
+ type: 'car-destroyed',
83
+ at: sim.time,
84
+ carId: car.id,
85
+ driverId: car.id,
86
+ reason: 'barrier',
87
+ });
88
+ }
@@ -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
+ }