@inventure71/paddockjs 2.0.1 → 4.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 (136) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +84 -512
  4. package/docs/architecture.md +38 -24
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/composable-layouts.md +77 -0
  7. package/docs/concepts.md +2 -2
  8. package/docs/custom_model_controller.md +6 -6
  9. package/docs/data-helpers.md +50 -0
  10. package/docs/data_contract.md +104 -27
  11. package/docs/getting-started.md +75 -0
  12. package/docs/headless-environment.md +68 -0
  13. package/docs/index.md +18 -4
  14. package/docs/learnings.md +6 -6
  15. package/docs/rules.md +12 -8
  16. package/docs/sense_contract.md +1 -1
  17. package/docs/system_specs.md +71 -21
  18. package/docs/theming.md +64 -0
  19. package/docs/training.md +26 -19
  20. package/docs/troubleshooting.md +76 -0
  21. package/docs/upgrading-hosts-to-3.md +42 -0
  22. package/examples/python/README.md +6 -0
  23. package/examples/python/base_policy_server.py +19 -0
  24. package/package.json +10 -3
  25. package/src/api/PaddockSimulatorController.js +150 -24
  26. package/src/api/runtimeTheme.js +59 -0
  27. package/src/app/F1SimulatorApp.js +213 -29
  28. package/src/app/banners/raceDataBannerController.js +71 -36
  29. package/src/app/banners/stewardMessageController.js +5 -4
  30. package/src/app/camera/cameraConstants.js +2 -0
  31. package/src/app/camera/cameraController.js +86 -29
  32. package/src/app/camera/cameraGeometry.js +12 -1
  33. package/src/app/domBindings.js +89 -9
  34. package/src/app/layoutScheduler.js +27 -0
  35. package/src/app/layoutSupport.js +165 -0
  36. package/src/app/raceOverlayClearanceSupport.js +158 -0
  37. package/src/app/readouts/carOverviewRenderer.js +16 -8
  38. package/src/app/readouts/controlStateRenderer.js +1 -1
  39. package/src/app/readouts/raceStatusRenderer.js +30 -21
  40. package/src/app/readouts/readoutFormatters.js +2 -0
  41. package/src/app/readouts/telemetryRenderer.js +17 -8
  42. package/src/app/readouts/timingTowerRenderer.js +262 -49
  43. package/src/app/runtime/frameLoop.js +5 -1
  44. package/src/app/runtime/runtimeVisibility.js +66 -5
  45. package/src/config/cssValues.js +16 -0
  46. package/src/config/defaultOptions.js +58 -38
  47. package/src/config/themeOptions.js +830 -0
  48. package/src/config/timingGapMode.js +13 -0
  49. package/src/config/urlOptions.js +25 -0
  50. package/src/data/championship.js +14 -9
  51. package/src/data/index.d.ts +212 -0
  52. package/src/data/index.js +14 -0
  53. package/src/environment/controllerLoop.js +17 -3
  54. package/src/environment/episode.js +11 -1
  55. package/src/environment/index.d.ts +1 -2
  56. package/src/environment/metrics.js +59 -16
  57. package/src/environment/observationVector.js +50 -33
  58. package/src/environment/observations.js +89 -20
  59. package/src/environment/options.js +1 -1
  60. package/src/environment/runtime.js +44 -11
  61. package/src/environment/scenarios.js +1 -1
  62. package/src/environment/sensors/carRays.js +48 -37
  63. package/src/environment/sensors/index.js +501 -97
  64. package/src/environment/sensors/indexedRayBands.js +134 -46
  65. package/src/environment/sensors/rayBandTrace.js +1171 -0
  66. package/src/environment/sensors/rayChannels.js +108 -0
  67. package/src/environment/sensors/rayConfig.js +71 -25
  68. package/src/environment/sensors/rayGeometry.js +23 -17
  69. package/src/environment/sensors/sensorTargets.js +67 -1
  70. package/src/environment/sensors/surfaceRays.js +167 -118
  71. package/src/environment/sensors/trackRays.js +157 -48
  72. package/src/index.d.ts +153 -9
  73. package/src/index.js +52 -3
  74. package/src/simulation/collisionGeometry.js +260 -96
  75. package/src/simulation/driver/driverController.js +1 -1
  76. package/src/simulation/driver/edgeRecovery.js +1 -1
  77. package/src/simulation/driver/racingControls.js +2 -2
  78. package/src/simulation/driver/rejoinControls.js +2 -2
  79. package/src/simulation/pit/pitFlow.js +21 -9
  80. package/src/simulation/pit/pitKinematics.js +2 -2
  81. package/src/simulation/pit/pitQueue.js +5 -3
  82. package/src/simulation/pit/pitRouteMovement.js +9 -4
  83. package/src/simulation/pit/pitRouting.js +79 -46
  84. package/src/simulation/pit/pitState.js +1 -0
  85. package/src/simulation/pit/pitTireService.js +7 -3
  86. package/src/simulation/race/raceFinish.js +25 -8
  87. package/src/simulation/race/raceOrder.js +189 -32
  88. package/src/simulation/race/raceProgress.js +129 -37
  89. package/src/simulation/race/raceSetup.js +4 -6
  90. package/src/simulation/race/raceStep.js +26 -14
  91. package/src/simulation/race/safetyCar.js +1 -1
  92. package/src/simulation/race/stalledDnf.js +1 -1
  93. package/src/simulation/raceSimulation.js +20 -17
  94. package/src/simulation/rules/collisionSteward.js +40 -23
  95. package/src/simulation/rules/rulesReview.js +77 -56
  96. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  97. package/src/simulation/timing/drsTiming.js +143 -30
  98. package/src/simulation/timing/gapEstimation.js +25 -3
  99. package/src/simulation/timing/lapTelemetry.js +138 -40
  100. package/src/simulation/timing/raceTiming.js +5 -1
  101. package/src/simulation/timing/sectorPerformance.js +104 -26
  102. package/src/simulation/timing/timingHistory.js +229 -14
  103. package/src/simulation/timing/timingLines.js +65 -7
  104. package/src/simulation/track/pitLaneLayout.js +30 -3
  105. package/src/simulation/track/pitLaneState.js +218 -19
  106. package/src/simulation/track/spatialQueries.js +66 -33
  107. package/src/simulation/track/trackMath.js +91 -24
  108. package/src/simulation/track/trackModel.js +2 -0
  109. package/src/simulation/track/trackQueryIndex.js +1468 -189
  110. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  111. package/src/simulation/track/trackQueryScratch.js +7 -0
  112. package/src/simulation/track/trackQueryStats.js +25 -0
  113. package/src/simulation/track/trackSnapshotJson.js +139 -0
  114. package/src/simulation/track/trackStatePolicy.js +249 -2
  115. package/src/simulation/vehicle/contactResolution.js +99 -62
  116. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  117. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  118. package/src/simulation/vehicle/runoffResponse.js +54 -29
  119. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  120. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  121. package/src/simulation/vehicle/vehiclePhysics.js +45 -30
  122. package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
  123. package/src/simulation/vehicle/vehicleState.js +7 -1
  124. package/src/simulation/vehicle/wheelSurface.js +807 -92
  125. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  126. package/src/styles.css +1568 -519
  127. package/src/ui/bannerTemplates.js +38 -28
  128. package/src/ui/cameraControlsTemplate.js +16 -5
  129. package/src/ui/carOverviewTemplate.js +12 -5
  130. package/src/ui/raceCanvasTemplate.js +31 -8
  131. package/src/ui/raceControlsTemplate.js +27 -10
  132. package/src/ui/shellTemplate.js +3 -1
  133. package/src/ui/telemetryDrawerTemplate.js +23 -10
  134. package/src/ui/telemetryTemplates.js +58 -32
  135. package/src/ui/templateUtils.js +48 -0
  136. package/src/ui/timingTowerTemplate.js +29 -12
@@ -1,27 +1,383 @@
1
- import { getVehicleGeometryState } from './vehicleGeometry.js';
2
- import { analyticWheelState, wheelFullyOutside } from './mainTrackWheelSurface.js';
3
- import { analyticPitWheelState, canUseAnalyticPitWheels, isNearPitConnector, patchSamples } from './pitWheelSurface.js';
4
- import { getEffectiveSurface, worstState } from './surfacePriority.js';
5
- import { nearestTrackStateForCar, pitOverrideAllowedForCar } from '../track/trackStatePolicy.js';
1
+ import { ensureOrientedRectCorners, getCurrentVehicleGeometryState } from './vehicleGeometry.js';
2
+ import {
3
+ analyticWheelProjectedHalfWidth,
4
+ wheelFullyOutside,
5
+ writeAnalyticWheelState,
6
+ writeAnalyticWheelStateWithProjectedHalfWidth,
7
+ } from './mainTrackWheelSurface.js';
8
+ import {
9
+ canUseAnalyticPitWheels,
10
+ canSkipConnectorPitChecks,
11
+ isNearPitConnector,
12
+ pitPatchInsideRoad,
13
+ writeAnalyticPitWheelState,
14
+ } from './pitWheelSurface.js';
15
+ import { getEffectiveSurface, priority, worstState } from './surfacePriority.js';
16
+ import {
17
+ nearestTrackStateForCar,
18
+ pitOverrideAllowedForCar,
19
+ queryHintedTrackStateForCar,
20
+ queryLocalSegmentTrackProjectionsForCar,
21
+ queryLocalSegmentTrackStateForCar,
22
+ queryLocalSegmentTrackStatesForCar,
23
+ queryRunoffTrackStateForCar,
24
+ writeLocalSegmentTrackStatesFromProjections,
25
+ } from '../track/trackStatePolicy.js';
6
26
 
7
- function finiteSignatureValue(value) {
8
- return Number.isFinite(value) ? Number(value).toFixed(4) : '';
27
+ export { getEffectiveSurface } from './surfacePriority.js';
28
+
29
+ const STRAIGHT_CONNECTOR_CURVATURE_EPSILON = 1e-8;
30
+ const MAIN_TRACK_PATCH_MARGIN = 0.001;
31
+
32
+ function usesAutomaticCenterState(centerState, cacheAsAuto = false) {
33
+ return cacheAsAuto || centerState == null;
9
34
  }
10
35
 
11
- function centerStateSignature(centerState) {
12
- if (!centerState) return 'auto';
13
- return [
14
- finiteSignatureValue(centerState.distance),
15
- finiteSignatureValue(centerState.signedOffset),
16
- finiteSignatureValue(centerState.crossTrackError),
17
- centerState.surface ?? '',
18
- centerState.inPitLane ? 1 : 0,
19
- centerState.pitLanePart ?? '',
20
- centerState.pitBoxId ?? '',
21
- ].join(':');
36
+ function cacheMatchesCenterState(cache, centerState, cacheAsAuto = false) {
37
+ const automatic = usesAutomaticCenterState(centerState, cacheAsAuto);
38
+ if ((cache.centerStateAuto ?? true) !== automatic) return false;
39
+ if (automatic) return true;
40
+ return cache.centerStateDistance === centerState.distance &&
41
+ cache.centerStateSignedOffset === centerState.signedOffset &&
42
+ cache.centerStateCrossTrackError === centerState.crossTrackError &&
43
+ cache.centerStateSurface === (centerState.surface ?? null) &&
44
+ cache.centerStateInPitLane === Boolean(centerState.inPitLane) &&
45
+ cache.centerStatePitLanePart === (centerState.pitLanePart ?? null) &&
46
+ cache.centerStatePitBoxId === (centerState.pitBoxId ?? null);
22
47
  }
23
48
 
24
- export { getEffectiveSurface } from './surfacePriority.js';
49
+ function writeCachedCenterState(cache, centerState, cacheAsAuto = false) {
50
+ const automatic = usesAutomaticCenterState(centerState, cacheAsAuto);
51
+ cache.centerStateAuto = automatic;
52
+ if (automatic) {
53
+ cache.centerStateDistance = null;
54
+ cache.centerStateSignedOffset = null;
55
+ cache.centerStateCrossTrackError = null;
56
+ cache.centerStateSurface = null;
57
+ cache.centerStateInPitLane = false;
58
+ cache.centerStatePitLanePart = null;
59
+ cache.centerStatePitBoxId = null;
60
+ return cache;
61
+ }
62
+ cache.centerStateDistance = centerState.distance;
63
+ cache.centerStateSignedOffset = centerState.signedOffset;
64
+ cache.centerStateCrossTrackError = centerState.crossTrackError;
65
+ cache.centerStateSurface = centerState.surface ?? null;
66
+ cache.centerStateInPitLane = Boolean(centerState.inPitLane);
67
+ cache.centerStatePitLanePart = centerState.pitLanePart ?? null;
68
+ cache.centerStatePitBoxId = centerState.pitBoxId ?? null;
69
+ return cache;
70
+ }
71
+
72
+ function canRefreshAutomaticCenterStateFromTrackState(car, track) {
73
+ const trackState = car?.trackState;
74
+ if (!trackState || trackState.inPitLane) return false;
75
+ if (
76
+ car?.trackStatePoseX !== car?.x ||
77
+ car?.trackStatePoseY !== car?.y ||
78
+ car?.trackStatePoseHeading !== car?.heading
79
+ ) {
80
+ return false;
81
+ }
82
+ const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
83
+ return Number.isFinite(trackState.crossTrackError) && trackState.crossTrackError <= mainRoadEdge;
84
+ }
85
+
86
+ function writeRepresentativeTrackState(target, centerState, effectiveSurface, representative, onTrack, inPitLane) {
87
+ target.segmentId = centerState.segmentId;
88
+ target.x = centerState.x;
89
+ target.y = centerState.y;
90
+ target.distance = centerState.distance;
91
+ target.heading = centerState.heading;
92
+ target.normalX = centerState.normalX;
93
+ target.normalY = centerState.normalY;
94
+ target.curvature = centerState.curvature;
95
+ target.signedOffset = representative.signedOffset;
96
+ target.crossTrackError = representative.crossTrackError;
97
+ target.surface = effectiveSurface;
98
+ target.onTrack = onTrack;
99
+ target.distanceSquared = centerState.distanceSquared;
100
+ target.inPitLane = inPitLane;
101
+ target.pitLanePart = centerState.pitLanePart;
102
+ target.pitBoxId = centerState.pitBoxId;
103
+ target.mainTrackSignedOffset = centerState.mainTrackSignedOffset;
104
+ target.mainTrackCrossTrackError = centerState.mainTrackCrossTrackError;
105
+ target.pitLaneCrossTrackError = centerState.pitLaneCrossTrackError;
106
+ target.wheelSurface = effectiveSurface;
107
+ return target;
108
+ }
109
+
110
+ function prepareScratchWheels(scratch, count) {
111
+ if (!scratch) return null;
112
+ const wheels = scratch.wheels ?? [];
113
+ const wheelPool = scratch.wheelPool ?? [];
114
+ scratch.wheels = wheels;
115
+ scratch.wheelPool = wheelPool;
116
+ wheels.length = count;
117
+ for (let index = 0; index < count; index += 1) {
118
+ const wheel = wheelPool[index] ?? {};
119
+ wheelPool[index] = wheel;
120
+ wheels[index] = wheel;
121
+ }
122
+ return wheels;
123
+ }
124
+
125
+ function recordScratchlessScalarWheelBatch(car, wheelCount) {
126
+ const stats = car?.runtimeBenchmarkStats;
127
+ if (!stats) return;
128
+ stats.scratchlessScalarWheelBatches = (stats.scratchlessScalarWheelBatches ?? 0) + 1;
129
+ stats.scratchlessScalarWheelWrites = (stats.scratchlessScalarWheelWrites ?? 0) + wheelCount;
130
+ }
131
+
132
+ function createWheelTargets(count) {
133
+ const wheels = new Array(count);
134
+ for (let index = 0; index < count; index += 1) {
135
+ wheels[index] = {};
136
+ }
137
+ return wheels;
138
+ }
139
+
140
+ function writeFullWheelState(target, patch, sampledStates, trackLimit) {
141
+ const state = worstState(sampledStates);
142
+ let minimumSignedOffset = Infinity;
143
+ let maximumSignedOffset = -Infinity;
144
+ for (let index = 0; index < sampledStates.length; index += 1) {
145
+ const signedOffset = sampledStates[index].signedOffset;
146
+ if (signedOffset < minimumSignedOffset) minimumSignedOffset = signedOffset;
147
+ if (signedOffset > maximumSignedOffset) maximumSignedOffset = signedOffset;
148
+ }
149
+ const outside = wheelFullyOutside(sampledStates, trackLimit);
150
+
151
+ target.id = patch.id;
152
+ target.x = patch.center.x;
153
+ target.y = patch.center.y;
154
+ target.signedOffset = state.signedOffset;
155
+ target.crossTrackError = state.crossTrackError;
156
+ target.surface = state.surface;
157
+ target.onTrack = Boolean(state.onTrack);
158
+ target.inPitLane = Boolean(state.inPitLane);
159
+ target.pitLanePart = state.pitLanePart ?? null;
160
+ target.pitBoxId = state.pitBoxId ?? null;
161
+ target.minimumSignedOffset = minimumSignedOffset;
162
+ target.maximumSignedOffset = maximumSignedOffset;
163
+ target.fullyOutsideWhiteLine = outside.fullyOutsideWhiteLine;
164
+ target.outsideSide = outside.outsideSide;
165
+ target.sampledStates = sampledStates;
166
+ return target;
167
+ }
168
+
169
+ function resetWheelSummary(summary) {
170
+ const trackLimits = summary.trackLimits ?? {
171
+ violating: false,
172
+ side: 0,
173
+ outsideBy: 0,
174
+ };
175
+ summary.effectiveSurface = 'track';
176
+ summary.effectivePriority = priority(summary.effectiveSurface);
177
+ summary.representative = null;
178
+ summary.allRightOutside = true;
179
+ summary.allLeftOutside = true;
180
+ summary.allOnTrackOrPit = true;
181
+ summary.allInPitLane = true;
182
+ summary.minimumSignedOffset = Infinity;
183
+ summary.maximumSignedOffset = -Infinity;
184
+ summary.trackLimits = trackLimits;
185
+ return summary;
186
+ }
187
+
188
+ function prepareWheelSummary(scratch) {
189
+ if (!scratch) return null;
190
+ const summary = scratch.summary ?? {};
191
+ scratch.summary = summary;
192
+ return resetWheelSummary(summary);
193
+ }
194
+
195
+ function recordWheelSummary(summary, wheel) {
196
+ const wheelPriority = priority(wheel.surface);
197
+ if (wheelPriority > summary.effectivePriority) {
198
+ summary.effectivePriority = wheelPriority;
199
+ summary.effectiveSurface = wheel.surface;
200
+ }
201
+ if (!summary.representative || Math.abs(wheel.signedOffset) > Math.abs(summary.representative.signedOffset)) {
202
+ summary.representative = wheel;
203
+ }
204
+ if (wheel.outsideSide !== 1) summary.allRightOutside = false;
205
+ if (wheel.outsideSide !== -1) summary.allLeftOutside = false;
206
+ if (!(wheel.onTrack || wheel.inPitLane)) summary.allOnTrackOrPit = false;
207
+ if (!wheel.inPitLane) summary.allInPitLane = false;
208
+ if (wheel.minimumSignedOffset < summary.minimumSignedOffset) summary.minimumSignedOffset = wheel.minimumSignedOffset;
209
+ if (wheel.maximumSignedOffset > summary.maximumSignedOffset) summary.maximumSignedOffset = wheel.maximumSignedOffset;
210
+ return summary;
211
+ }
212
+
213
+ function finishWheelSummary(summary, track, trackLimit = track?.width / 2) {
214
+ const trackLimits = summary.trackLimits;
215
+ trackLimits.violating = false;
216
+ trackLimits.side = 0;
217
+ trackLimits.outsideBy = 0;
218
+ if (track && (summary.allRightOutside || summary.allLeftOutside)) {
219
+ const side = summary.allRightOutside ? 1 : -1;
220
+ const outsideBy = summary.allRightOutside
221
+ ? summary.minimumSignedOffset - trackLimit
222
+ : -trackLimit - summary.maximumSignedOffset;
223
+ trackLimits.violating = outsideBy > 0;
224
+ trackLimits.side = side;
225
+ trackLimits.outsideBy = Math.max(0, outsideBy);
226
+ }
227
+ return summary;
228
+ }
229
+
230
+ function queryConnectorSampleState(track, car, point, progressHint, centerState) {
231
+ const baseSegmentId = Number.isInteger(centerState?.segmentId) ? centerState.segmentId : null;
232
+ if (baseSegmentId == null) return queryHintedTrackStateForCar(track, car, point, progressHint);
233
+ return queryLocalSegmentTrackStateForCar(
234
+ track,
235
+ car,
236
+ point,
237
+ baseSegmentId,
238
+ progressHint,
239
+ { skipPitOverrideInsideMainRoad: true },
240
+ );
241
+ }
242
+
243
+ function sampleFullConnectorPatchInto(sampledStates, patch, track, car, progressHint, centerState) {
244
+ const corners = ensureOrientedRectCorners(patch);
245
+ sampledStates.length = 1 + corners.length;
246
+ sampledStates[0] = queryConnectorSampleState(track, car, patch.center, progressHint, centerState);
247
+ for (let index = 0; index < corners.length; index += 1) {
248
+ sampledStates[index + 1] = queryConnectorSampleState(track, car, corners[index], progressHint, centerState);
249
+ }
250
+ return sampledStates;
251
+ }
252
+
253
+ function sampleFullNearestPatchInto(sampledStates, patch, track, car) {
254
+ const corners = ensureOrientedRectCorners(patch);
255
+ sampledStates.length = 1 + corners.length;
256
+ sampledStates[0] = nearestTrackStateForCar(track, car, patch.center, car.progress);
257
+ for (let index = 0; index < corners.length; index += 1) {
258
+ sampledStates[index + 1] = nearestTrackStateForCar(track, car, corners[index], car.progress);
259
+ }
260
+ return sampledStates;
261
+ }
262
+
263
+ function writeConnectorWheelState(target, {
264
+ patch,
265
+ wheelCenterState,
266
+ centerState,
267
+ car,
268
+ track,
269
+ trackLimit,
270
+ progressHint,
271
+ sampledStates,
272
+ }) {
273
+ if (!wheelCenterState) {
274
+ return writeAnalyticWheelState(target, patch, centerState, track, trackLimit);
275
+ }
276
+ if (!wheelCenterState.inPitLane) {
277
+ return writeAnalyticWheelState(target, patch, wheelCenterState, track, trackLimit);
278
+ }
279
+ if (pitPatchInsideRoad(patch, wheelCenterState)) {
280
+ return writeAnalyticPitWheelState(target, patch, wheelCenterState);
281
+ }
282
+ sampleFullConnectorPatchInto(sampledStates, patch, track, car, progressHint, centerState);
283
+ return writeFullWheelState(target, patch, sampledStates, trackLimit);
284
+ }
285
+
286
+ function collectPatchCenters(target, patches) {
287
+ target.length = patches.length;
288
+ for (let index = 0; index < patches.length; index += 1) {
289
+ target[index] = patches[index].center;
290
+ }
291
+ return target;
292
+ }
293
+
294
+ function connectorWheelCenterStates(
295
+ track,
296
+ car,
297
+ patches,
298
+ centerState,
299
+ progressHint,
300
+ scratchStates = null,
301
+ scratchProjections = null,
302
+ scratchPositions = null,
303
+ ) {
304
+ const baseSegmentId = Number.isInteger(centerState?.segmentId) ? centerState.segmentId : null;
305
+ const states = scratchStates ?? new Array(patches.length);
306
+ states.length = patches.length;
307
+ if (baseSegmentId == null) {
308
+ for (let index = 0; index < patches.length; index += 1) {
309
+ states[index] = queryHintedTrackStateForCar(track, car, patches[index].center, progressHint);
310
+ }
311
+ return states;
312
+ }
313
+ const positions = collectPatchCenters(scratchPositions ?? new Array(patches.length), patches);
314
+ return queryLocalSegmentTrackStatesForCar(
315
+ track,
316
+ car,
317
+ positions,
318
+ baseSegmentId,
319
+ progressHint,
320
+ {
321
+ radius: 2,
322
+ skipPitOverrideInsideMainRoad: true,
323
+ target: states,
324
+ projections: scratchProjections,
325
+ },
326
+ );
327
+ }
328
+
329
+ function connectorWheelCenterProjections(
330
+ track,
331
+ car,
332
+ patches,
333
+ centerState,
334
+ progressHint,
335
+ scratchProjections = null,
336
+ scratchPositions = null,
337
+ ) {
338
+ const baseSegmentId = Number.isInteger(centerState?.segmentId) ? centerState.segmentId : null;
339
+ if (baseSegmentId == null) return null;
340
+ const positions = collectPatchCenters(scratchPositions ?? new Array(patches.length), patches);
341
+ return queryLocalSegmentTrackProjectionsForCar(
342
+ track,
343
+ car,
344
+ positions,
345
+ baseSegmentId,
346
+ progressHint,
347
+ {
348
+ radius: 2,
349
+ projections: scratchProjections,
350
+ },
351
+ );
352
+ }
353
+
354
+ function canUseStraightConnectorMainTrackAnalytic(track, geometry, centerState, trackLimit) {
355
+ if (
356
+ !track ||
357
+ !geometry?.contactPatches?.length ||
358
+ !centerState ||
359
+ centerState.inPitLane ||
360
+ !centerState.onTrack ||
361
+ Math.abs(centerState.curvature ?? 0) > STRAIGHT_CONNECTOR_CURVATURE_EPSILON
362
+ ) {
363
+ return false;
364
+ }
365
+
366
+ for (let index = 0; index < geometry.contactPatches.length; index += 1) {
367
+ const patch = geometry.contactPatches[index];
368
+ const wheelCenterOffset =
369
+ (patch.center.x - centerState.x) * centerState.normalX +
370
+ (patch.center.y - centerState.y) * centerState.normalY;
371
+ const projectedHalfWidth = analyticWheelProjectedHalfWidth(patch, centerState);
372
+ if (
373
+ wheelCenterOffset - projectedHalfWidth < -trackLimit - MAIN_TRACK_PATCH_MARGIN ||
374
+ wheelCenterOffset + projectedHalfWidth > trackLimit + MAIN_TRACK_PATCH_MARGIN
375
+ ) {
376
+ return false;
377
+ }
378
+ }
379
+ return true;
380
+ }
25
381
 
26
382
  export function isWholeCarOutsideTrackLimits(wheels = [], track, relaxedMargin = 0) {
27
383
  if (!wheels.length || !track) {
@@ -32,8 +388,17 @@ export function isWholeCarOutsideTrackLimits(wheels = [], track, relaxedMargin =
32
388
  };
33
389
  }
34
390
  const trackLimit = track.width / 2 + relaxedMargin;
35
- const right = wheels.every((wheel) => wheel.outsideSide === 1);
36
- const left = wheels.every((wheel) => wheel.outsideSide === -1);
391
+ let right = true;
392
+ let left = true;
393
+ let minimumSignedOffset = Infinity;
394
+ let maximumSignedOffset = -Infinity;
395
+ for (let index = 0; index < wheels.length; index += 1) {
396
+ const wheel = wheels[index];
397
+ if (wheel.outsideSide !== 1) right = false;
398
+ if (wheel.outsideSide !== -1) left = false;
399
+ if (wheel.minimumSignedOffset < minimumSignedOffset) minimumSignedOffset = wheel.minimumSignedOffset;
400
+ if (wheel.maximumSignedOffset > maximumSignedOffset) maximumSignedOffset = wheel.maximumSignedOffset;
401
+ }
37
402
  if (!right && !left) {
38
403
  return {
39
404
  violating: false,
@@ -43,8 +408,8 @@ export function isWholeCarOutsideTrackLimits(wheels = [], track, relaxedMargin =
43
408
  }
44
409
  const side = right ? 1 : -1;
45
410
  const outsideBy = right
46
- ? Math.min(...wheels.map((wheel) => wheel.minimumSignedOffset)) - trackLimit
47
- : -trackLimit - Math.max(...wheels.map((wheel) => wheel.maximumSignedOffset));
411
+ ? minimumSignedOffset - trackLimit
412
+ : -trackLimit - maximumSignedOffset;
48
413
 
49
414
  return {
50
415
  violating: outsideBy > 0,
@@ -53,82 +418,419 @@ export function isWholeCarOutsideTrackLimits(wheels = [], track, relaxedMargin =
53
418
  };
54
419
  }
55
420
 
56
- export function calculateWheelSurfaceState({ car, track, centerState: providedCenterState = null }) {
57
- const geometry = getVehicleGeometryState(car);
421
+ function writeAnalyticWheelStatesWithSummary(targetWheels, patches, centerState, track, trackLimit, summaryTarget = null) {
422
+ const sharedProjectedHalfWidth = patches.length > 0
423
+ ? analyticWheelProjectedHalfWidth(patches[0], centerState)
424
+ : 0;
425
+ const summary = resetWheelSummary(summaryTarget ?? {});
426
+
427
+ for (let index = 0; index < patches.length; index += 1) {
428
+ const wheel = writeAnalyticWheelStateWithProjectedHalfWidth(
429
+ targetWheels[index],
430
+ patches[index],
431
+ centerState,
432
+ track,
433
+ trackLimit,
434
+ sharedProjectedHalfWidth,
435
+ );
436
+ recordWheelSummary(summary, wheel);
437
+ }
438
+ finishWheelSummary(summary, track, trackLimit);
439
+
440
+ return {
441
+ wheels: targetWheels,
442
+ effectiveSurface: summary.effectiveSurface,
443
+ representative: summary.representative,
444
+ trackLimits: summary.trackLimits,
445
+ allOnTrackOrPit: summary.allOnTrackOrPit,
446
+ allInPitLane: summary.allInPitLane,
447
+ };
448
+ }
449
+
450
+ function writeAnalyticPitWheelStatesWithSummary(targetWheels, patches, centerState, track, trackLimit, summaryTarget = null) {
451
+ const summary = resetWheelSummary(summaryTarget ?? {});
452
+
453
+ for (let index = 0; index < patches.length; index += 1) {
454
+ const wheel = writeAnalyticPitWheelState(targetWheels[index], patches[index], centerState);
455
+ recordWheelSummary(summary, wheel);
456
+ }
457
+ finishWheelSummary(summary, track, trackLimit);
458
+
459
+ return {
460
+ wheels: targetWheels,
461
+ effectiveSurface: summary.effectiveSurface,
462
+ representative: summary.representative,
463
+ trackLimits: summary.trackLimits,
464
+ allOnTrackOrPit: summary.allOnTrackOrPit,
465
+ allInPitLane: summary.allInPitLane,
466
+ };
467
+ }
468
+
469
+ function writeAnalyticWheelStatesFromTrackStatesWithSummary(
470
+ targetWheels,
471
+ patches,
472
+ wheelTrackStates,
473
+ track,
474
+ trackLimit,
475
+ summaryTarget = null,
476
+ ) {
477
+ const summary = resetWheelSummary(summaryTarget ?? {});
478
+
479
+ for (let index = 0; index < patches.length; index += 1) {
480
+ const wheel = writeAnalyticWheelState(
481
+ targetWheels[index],
482
+ patches[index],
483
+ wheelTrackStates[index],
484
+ track,
485
+ trackLimit,
486
+ );
487
+ recordWheelSummary(summary, wheel);
488
+ }
489
+ finishWheelSummary(summary, track, trackLimit);
490
+
491
+ return {
492
+ wheels: targetWheels,
493
+ effectiveSurface: summary.effectiveSurface,
494
+ representative: summary.representative,
495
+ trackLimits: summary.trackLimits,
496
+ allOnTrackOrPit: summary.allOnTrackOrPit,
497
+ allInPitLane: summary.allInPitLane,
498
+ };
499
+ }
500
+
501
+ function writeConnectorWheelStatesWithSummary({
502
+ targetWheels,
503
+ patches,
504
+ wheelCenterStates,
505
+ centerState,
506
+ car,
507
+ track,
508
+ trackLimit,
509
+ progressHint,
510
+ sampledStatePools,
511
+ summaryTarget = null,
512
+ }) {
513
+ const summary = resetWheelSummary(summaryTarget ?? {});
514
+ for (let index = 0; index < patches.length; index += 1) {
515
+ const sampledStates = sampledStatePools[index] ?? [];
516
+ sampledStatePools[index] = sampledStates;
517
+ const wheel = writeConnectorWheelState(targetWheels[index], {
518
+ patch: patches[index],
519
+ wheelCenterState: wheelCenterStates[index],
520
+ centerState,
521
+ car,
522
+ track,
523
+ trackLimit,
524
+ progressHint,
525
+ sampledStates,
526
+ });
527
+ recordWheelSummary(summary, wheel);
528
+ }
529
+ finishWheelSummary(summary, track, trackLimit);
530
+
531
+ return {
532
+ wheels: targetWheels,
533
+ effectiveSurface: summary.effectiveSurface,
534
+ representative: summary.representative,
535
+ trackLimits: summary.trackLimits,
536
+ allOnTrackOrPit: summary.allOnTrackOrPit,
537
+ allInPitLane: summary.allInPitLane,
538
+ };
539
+ }
540
+
541
+ function writeNearestSampledWheelStatesWithSummary({
542
+ targetWheels,
543
+ patches,
544
+ car,
545
+ track,
546
+ trackLimit,
547
+ sampledStatePools,
548
+ summaryTarget = null,
549
+ }) {
550
+ const summary = resetWheelSummary(summaryTarget ?? {});
551
+ for (let index = 0; index < patches.length; index += 1) {
552
+ const patch = patches[index];
553
+ const sampledStates = sampledStatePools[index] ?? [];
554
+ sampledStatePools[index] = sampledStates;
555
+ sampleFullNearestPatchInto(sampledStates, patch, track, car);
556
+ const wheel = writeFullWheelState(targetWheels[index], patch, sampledStates, trackLimit);
557
+ recordWheelSummary(summary, wheel);
558
+ }
559
+ finishWheelSummary(summary, track, trackLimit);
560
+
561
+ return {
562
+ wheels: targetWheels,
563
+ effectiveSurface: summary.effectiveSurface,
564
+ representative: summary.representative,
565
+ trackLimits: summary.trackLimits,
566
+ allOnTrackOrPit: summary.allOnTrackOrPit,
567
+ allInPitLane: summary.allInPitLane,
568
+ };
569
+ }
570
+
571
+ export function calculateWheelSurfaceState({
572
+ car,
573
+ track,
574
+ centerState: providedCenterState = null,
575
+ geometry: providedGeometry = null,
576
+ scratch = null,
577
+ allowPitOverride: providedAllowPitOverride = null,
578
+ trackStateTarget = null,
579
+ }) {
580
+ const geometry = providedGeometry ?? getCurrentVehicleGeometryState(car);
58
581
  const trackLimit = track.width / 2;
59
- const allowPitOverride = pitOverrideAllowedForCar(car);
582
+ const allowPitOverride = providedAllowPitOverride ?? pitOverrideAllowedForCar(car);
60
583
  const centerState = providedCenterState == null
61
- ? nearestTrackStateForCar(track, car)
584
+ ? canRefreshAutomaticCenterStateFromTrackState(car, track)
585
+ ? car.trackState
586
+ : nearestTrackStateForCar(track, car, car, car.progress, { allowPitOverride })
62
587
  : !allowPitOverride && providedCenterState.inPitLane
63
588
  ? nearestTrackStateForCar(track, car, car, car.progress, { allowPitOverride: false })
64
589
  : providedCenterState;
590
+ const nearPitConnector = !centerState.inPitLane && isNearPitConnector(track, centerState);
591
+ const scratchWheels = prepareScratchWheels(scratch, geometry.contactPatches.length);
592
+ const scratchSummary = prepareWheelSummary(scratch);
593
+
594
+ if (!centerState.inPitLane && !nearPitConnector) {
595
+ const wheels = scratchWheels ?? createWheelTargets(geometry.contactPatches.length);
596
+ if (!scratchWheels) recordScratchlessScalarWheelBatch(car, geometry.contactPatches.length);
597
+ const summary = writeAnalyticWheelStatesWithSummary(
598
+ wheels,
599
+ geometry.contactPatches,
600
+ centerState,
601
+ track,
602
+ trackLimit,
603
+ scratchSummary,
604
+ );
605
+ return {
606
+ wheels,
607
+ effectiveSurface: summary.effectiveSurface,
608
+ trackLimits: summary.trackLimits,
609
+ sampleMode: 'analytic',
610
+ representativeState: writeRepresentativeTrackState(
611
+ trackStateTarget ?? {},
612
+ centerState,
613
+ summary.effectiveSurface,
614
+ summary.representative,
615
+ summary.allOnTrackOrPit,
616
+ false,
617
+ ),
618
+ };
619
+ }
620
+
65
621
  const useAnalyticPitSampling = canUseAnalyticPitWheels(geometry, centerState);
66
- const useFullSampling = !useAnalyticPitSampling && Boolean(centerState.inPitLane || isNearPitConnector(track, centerState));
67
- const wheels = useAnalyticPitSampling
68
- ? geometry.contactPatches.map((patch) => analyticPitWheelState(patch, centerState))
69
- : useFullSampling
70
- ? geometry.contactPatches.map((patch) => {
71
- const sampleState = (point) => nearestTrackStateForCar(track, car, point, car.progress);
72
- const sampledStates = patchSamples(patch).map(sampleState);
73
- const state = worstState(sampledStates);
74
- const offsets = sampledStates.map((sample) => sample.signedOffset);
75
- const outside = wheelFullyOutside(sampledStates, trackLimit);
76
-
77
- return {
78
- id: patch.id,
79
- x: patch.center.x,
80
- y: patch.center.y,
81
- signedOffset: state.signedOffset,
82
- crossTrackError: state.crossTrackError,
83
- surface: state.surface,
84
- onTrack: Boolean(state.onTrack),
85
- inPitLane: Boolean(state.inPitLane),
86
- pitLanePart: state.pitLanePart ?? null,
87
- pitBoxId: state.pitBoxId ?? null,
88
- minimumSignedOffset: Math.min(...offsets),
89
- maximumSignedOffset: Math.max(...offsets),
90
- fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
91
- outsideSide: outside.outsideSide,
92
- sampledStates,
93
- };
94
- })
95
- : geometry.contactPatches.map((patch) => analyticWheelState(patch, centerState, track, trackLimit));
96
- const effectiveSurface = getEffectiveSurface(wheels);
97
- const representative = wheels.reduce((best, wheel) => (
98
- Math.abs(wheel.signedOffset) > Math.abs(best.signedOffset) ? wheel : best
99
- ), wheels[0]);
100
- const trackLimits = isWholeCarOutsideTrackLimits(wheels, track);
622
+ const useConnectorSampling = !useAnalyticPitSampling && nearPitConnector;
623
+ const connectorPitChecksSkipped = useConnectorSampling && canSkipConnectorPitChecks(track, geometry, centerState);
624
+ const connectorStraightMainTrackAnalytic = useConnectorSampling &&
625
+ !connectorPitChecksSkipped &&
626
+ canUseStraightConnectorMainTrackAnalytic(track, geometry, centerState, trackLimit);
627
+ const useFullSampling = !useAnalyticPitSampling && !useConnectorSampling && centerState.inPitLane;
628
+ const connectorProgressHint = centerState.distance ?? car.progress ?? null;
629
+ let analyticSummary = null;
630
+ const wheels = scratchWheels ?? createWheelTargets(geometry.contactPatches.length);
631
+ if (scratchWheels) {
632
+ if (useAnalyticPitSampling) {
633
+ analyticSummary = writeAnalyticPitWheelStatesWithSummary(
634
+ scratchWheels,
635
+ geometry.contactPatches,
636
+ centerState,
637
+ track,
638
+ trackLimit,
639
+ scratchSummary,
640
+ );
641
+ } else if (connectorPitChecksSkipped || connectorStraightMainTrackAnalytic) {
642
+ analyticSummary = writeAnalyticWheelStatesWithSummary(
643
+ scratchWheels,
644
+ geometry.contactPatches,
645
+ centerState,
646
+ track,
647
+ trackLimit,
648
+ scratchSummary,
649
+ );
650
+ } else if (useConnectorSampling) {
651
+ const sampledStatePools = scratch.sampledStatePools ?? [];
652
+ const connectorProjectionPools = scratch.connectorProjectionPools ?? [];
653
+ const connectorPositionPools = scratch.connectorPositionPools ?? [];
654
+ scratch.sampledStatePools = sampledStatePools;
655
+ scratch.connectorProjectionPools = connectorProjectionPools;
656
+ scratch.connectorPositionPools = connectorPositionPools;
657
+ const connectorQueriedStatePools = scratch.connectorQueriedStatePools ?? [];
658
+ scratch.connectorQueriedStatePools = connectorQueriedStatePools;
659
+ const wheelCenterProjections = connectorWheelCenterProjections(
660
+ track,
661
+ car,
662
+ geometry.contactPatches,
663
+ centerState,
664
+ connectorProgressHint,
665
+ connectorProjectionPools,
666
+ connectorPositionPools,
667
+ );
668
+ const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
669
+ let allWheelCentersOnMainTrack = Array.isArray(wheelCenterProjections);
670
+ for (let index = 0; allWheelCentersOnMainTrack && index < wheelCenterProjections.length; index += 1) {
671
+ const projection = wheelCenterProjections[index];
672
+ if (!projection || projection.crossTrackError > mainRoadEdge) allWheelCentersOnMainTrack = false;
673
+ }
674
+ if (allWheelCentersOnMainTrack) {
675
+ connectorQueriedStatePools.length = 0;
676
+ analyticSummary = writeAnalyticWheelStatesFromTrackStatesWithSummary(
677
+ scratchWheels,
678
+ geometry.contactPatches,
679
+ wheelCenterProjections,
680
+ track,
681
+ trackLimit,
682
+ scratchSummary,
683
+ );
684
+ } else {
685
+ const wheelCenterStates = wheelCenterProjections
686
+ ? writeLocalSegmentTrackStatesFromProjections(
687
+ track,
688
+ connectorPositionPools,
689
+ connectorProgressHint,
690
+ wheelCenterProjections,
691
+ {
692
+ allowPitOverride,
693
+ skipPitOverrideInsideMainRoad: true,
694
+ target: connectorQueriedStatePools,
695
+ },
696
+ )
697
+ : connectorWheelCenterStates(
698
+ track,
699
+ car,
700
+ geometry.contactPatches,
701
+ centerState,
702
+ connectorProgressHint,
703
+ connectorQueriedStatePools,
704
+ connectorProjectionPools,
705
+ connectorPositionPools,
706
+ );
707
+ analyticSummary = writeConnectorWheelStatesWithSummary({
708
+ targetWheels: scratchWheels,
709
+ patches: geometry.contactPatches,
710
+ wheelCenterStates,
711
+ centerState,
712
+ car,
713
+ track,
714
+ trackLimit,
715
+ progressHint: connectorProgressHint,
716
+ sampledStatePools,
717
+ summaryTarget: scratchSummary,
718
+ });
719
+ }
720
+ } else if (useFullSampling) {
721
+ const sampledStatePools = scratch.sampledStatePools ?? [];
722
+ scratch.sampledStatePools = sampledStatePools;
723
+ analyticSummary = writeNearestSampledWheelStatesWithSummary({
724
+ targetWheels: scratchWheels,
725
+ patches: geometry.contactPatches,
726
+ car,
727
+ track,
728
+ trackLimit,
729
+ sampledStatePools,
730
+ summaryTarget: scratchSummary,
731
+ });
732
+ } else {
733
+ analyticSummary = writeAnalyticWheelStatesWithSummary(
734
+ scratchWheels,
735
+ geometry.contactPatches,
736
+ centerState,
737
+ track,
738
+ trackLimit,
739
+ scratchSummary,
740
+ );
741
+ }
742
+ } else if (useAnalyticPitSampling) {
743
+ recordScratchlessScalarWheelBatch(car, geometry.contactPatches.length);
744
+ analyticSummary = writeAnalyticPitWheelStatesWithSummary(
745
+ wheels,
746
+ geometry.contactPatches,
747
+ centerState,
748
+ track,
749
+ trackLimit,
750
+ );
751
+ } else if (connectorPitChecksSkipped || connectorStraightMainTrackAnalytic) {
752
+ recordScratchlessScalarWheelBatch(car, geometry.contactPatches.length);
753
+ analyticSummary = writeAnalyticWheelStatesWithSummary(
754
+ wheels,
755
+ geometry.contactPatches,
756
+ centerState,
757
+ track,
758
+ trackLimit,
759
+ );
760
+ } else if (useConnectorSampling) {
761
+ recordScratchlessScalarWheelBatch(car, geometry.contactPatches.length);
762
+ const wheelCenterStates = connectorWheelCenterStates(track, car, geometry.contactPatches, centerState, connectorProgressHint);
763
+ analyticSummary = writeConnectorWheelStatesWithSummary({
764
+ targetWheels: wheels,
765
+ patches: geometry.contactPatches,
766
+ wheelCenterStates,
767
+ centerState,
768
+ car,
769
+ track,
770
+ trackLimit,
771
+ progressHint: connectorProgressHint,
772
+ sampledStatePools: new Array(geometry.contactPatches.length),
773
+ });
774
+ } else if (useFullSampling) {
775
+ recordScratchlessScalarWheelBatch(car, geometry.contactPatches.length);
776
+ analyticSummary = writeNearestSampledWheelStatesWithSummary({
777
+ targetWheels: wheels,
778
+ patches: geometry.contactPatches,
779
+ car,
780
+ track,
781
+ trackLimit,
782
+ sampledStatePools: new Array(geometry.contactPatches.length),
783
+ });
784
+ } else {
785
+ recordScratchlessScalarWheelBatch(car, geometry.contactPatches.length);
786
+ analyticSummary = writeAnalyticWheelStatesWithSummary(
787
+ wheels,
788
+ geometry.contactPatches,
789
+ centerState,
790
+ track,
791
+ trackLimit,
792
+ );
793
+ }
794
+ const summary = analyticSummary;
795
+ const effectiveSurface = summary.effectiveSurface;
796
+ const representative = summary.representative;
797
+ const trackLimits = summary.trackLimits;
101
798
 
102
799
  return {
103
800
  wheels,
104
801
  effectiveSurface,
105
802
  trackLimits,
106
- sampleMode: useAnalyticPitSampling ? 'pit-analytic' : useFullSampling ? 'full' : 'analytic',
107
- representativeState: representative ? {
108
- ...centerState,
109
- surface: effectiveSurface,
110
- onTrack: wheels.every((wheel) => wheel.onTrack || wheel.inPitLane),
111
- inPitLane: wheels.every((wheel) => wheel.inPitLane),
112
- signedOffset: representative.signedOffset,
113
- crossTrackError: representative.crossTrackError,
114
- } : nearestTrackStateForCar(track, car),
803
+ sampleMode: useAnalyticPitSampling
804
+ ? 'pit-analytic'
805
+ : useConnectorSampling
806
+ ? 'connector-analytic'
807
+ : useFullSampling
808
+ ? 'full'
809
+ : 'analytic',
810
+ representativeState: representative
811
+ ? writeRepresentativeTrackState(
812
+ trackStateTarget ?? {},
813
+ centerState,
814
+ effectiveSurface,
815
+ representative,
816
+ summary.allOnTrackOrPit,
817
+ summary.allInPitLane,
818
+ )
819
+ : nearestTrackStateForCar(track, car),
115
820
  };
116
821
  }
117
822
 
118
823
  export function applyWheelSurfaceState(car, track, options = {}) {
119
- const geometry = getVehicleGeometryState(car);
120
- const cacheKey = {
121
- track,
122
- geometrySignature: geometry.signature,
123
- centerSignature: centerStateSignature(options.centerState),
124
- pitOverrideAllowed: pitOverrideAllowedForCar(car),
125
- };
824
+ const geometry = getCurrentVehicleGeometryState(car);
825
+ const allowPitOverride = options.allowPitOverride ?? pitOverrideAllowedForCar(car);
126
826
  const cached = car.wheelSurfaceCache;
127
827
  if (
128
- cached?.track === cacheKey.track &&
129
- cached.geometrySignature === cacheKey.geometrySignature &&
130
- cached.centerSignature === cacheKey.centerSignature &&
131
- cached.pitOverrideAllowed === cacheKey.pitOverrideAllowed
828
+ cached?.track === track &&
829
+ cached.geometryX === (geometry.pose?.x ?? null) &&
830
+ cached.geometryY === (geometry.pose?.y ?? null) &&
831
+ cached.geometryHeading === (geometry.pose?.heading ?? null) &&
832
+ cacheMatchesCenterState(cached, options.centerState, options.cacheAsAuto) &&
833
+ cached.pitOverrideAllowed === allowPitOverride
132
834
  ) {
133
835
  car.wheelStates = cached.result.wheels;
134
836
  car.trackLimitState = cached.result.trackLimits;
@@ -136,17 +838,30 @@ export function applyWheelSurfaceState(car, track, options = {}) {
136
838
  return cached.result;
137
839
  }
138
840
 
139
- const result = calculateWheelSurfaceState({ car, track, centerState: options.centerState });
841
+ const result = calculateWheelSurfaceState({
842
+ car,
843
+ track,
844
+ centerState: options.centerState,
845
+ geometry,
846
+ scratch: car.wheelSurfaceScratch ??= {},
847
+ allowPitOverride,
848
+ trackStateTarget: car.trackState ?? {},
849
+ });
140
850
  car.wheelStates = result.wheels;
141
851
  car.trackLimitState = result.trackLimits;
142
- car.trackState = {
143
- ...result.representativeState,
144
- wheelSurface: result.effectiveSurface,
145
- };
146
- car.wheelSurfaceCache = {
147
- ...cacheKey,
148
- result,
149
- trackState: car.trackState,
150
- };
852
+ car.trackState = result.representativeState;
853
+ car.trackStatePoseX = car.x;
854
+ car.trackStatePoseY = car.y;
855
+ car.trackStatePoseHeading = car.heading;
856
+ const cache = cached ?? {};
857
+ cache.track = track;
858
+ cache.geometryX = geometry.pose?.x ?? null;
859
+ cache.geometryY = geometry.pose?.y ?? null;
860
+ cache.geometryHeading = geometry.pose?.heading ?? null;
861
+ writeCachedCenterState(cache, options.centerState, options.cacheAsAuto);
862
+ cache.pitOverrideAllowed = allowPitOverride;
863
+ cache.result = result;
864
+ cache.trackState = car.trackState;
865
+ car.wheelSurfaceCache = cache;
151
866
  return result;
152
867
  }