@inventure71/paddockjs 3.0.0 → 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 (90) hide show
  1. package/CHANGELOG.md +146 -1
  2. package/INSTALL_AND_UPDATE.md +15 -4
  3. package/README.md +84 -562
  4. package/docs/architecture.md +28 -18
  5. package/docs/composable-layouts.md +77 -0
  6. package/docs/custom_model_controller.md +4 -4
  7. package/docs/data-helpers.md +50 -0
  8. package/docs/data_contract.md +9 -7
  9. package/docs/getting-started.md +75 -0
  10. package/docs/headless-environment.md +68 -0
  11. package/docs/index.md +17 -4
  12. package/docs/rules.md +6 -2
  13. package/docs/system_specs.md +49 -6
  14. package/docs/theming.md +64 -0
  15. package/docs/training.md +20 -14
  16. package/docs/troubleshooting.md +76 -0
  17. package/docs/upgrading-hosts-to-3.md +42 -0
  18. package/examples/python/README.md +6 -0
  19. package/examples/python/base_policy_server.py +19 -0
  20. package/package.json +10 -3
  21. package/src/api/PaddockSimulatorController.js +78 -22
  22. package/src/api/runtimeTheme.js +59 -0
  23. package/src/app/F1SimulatorApp.js +24 -3
  24. package/src/app/readouts/timingTowerRenderer.js +203 -42
  25. package/src/app/runtime/frameLoop.js +5 -1
  26. package/src/config/defaultOptions.js +8 -1
  27. package/src/config/themeOptions.js +5 -0
  28. package/src/data/index.d.ts +212 -0
  29. package/src/data/index.js +14 -0
  30. package/src/environment/episode.js +11 -1
  31. package/src/environment/metrics.js +59 -16
  32. package/src/environment/observationVector.js +50 -33
  33. package/src/environment/observations.js +89 -20
  34. package/src/environment/runtime.js +41 -0
  35. package/src/environment/sensors/carRays.js +48 -37
  36. package/src/environment/sensors/index.js +501 -97
  37. package/src/environment/sensors/indexedRayBands.js +134 -46
  38. package/src/environment/sensors/rayBandTrace.js +1171 -0
  39. package/src/environment/sensors/rayChannels.js +108 -0
  40. package/src/environment/sensors/rayConfig.js +71 -25
  41. package/src/environment/sensors/rayGeometry.js +23 -17
  42. package/src/environment/sensors/sensorTargets.js +67 -1
  43. package/src/environment/sensors/surfaceRays.js +130 -89
  44. package/src/environment/sensors/trackRays.js +115 -1
  45. package/src/index.d.ts +43 -0
  46. package/src/index.js +27 -2
  47. package/src/simulation/collisionGeometry.js +260 -96
  48. package/src/simulation/pit/pitFlow.js +21 -9
  49. package/src/simulation/pit/pitQueue.js +5 -3
  50. package/src/simulation/pit/pitRouteMovement.js +9 -4
  51. package/src/simulation/pit/pitRouting.js +77 -42
  52. package/src/simulation/pit/pitState.js +1 -0
  53. package/src/simulation/pit/pitTireService.js +7 -3
  54. package/src/simulation/race/raceFinish.js +25 -8
  55. package/src/simulation/race/raceOrder.js +189 -32
  56. package/src/simulation/race/raceProgress.js +129 -37
  57. package/src/simulation/race/raceSetup.js +2 -0
  58. package/src/simulation/race/raceStep.js +25 -13
  59. package/src/simulation/raceSimulation.js +20 -17
  60. package/src/simulation/rules/collisionSteward.js +40 -23
  61. package/src/simulation/rules/rulesReview.js +77 -56
  62. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  63. package/src/simulation/timing/drsTiming.js +143 -30
  64. package/src/simulation/timing/gapEstimation.js +25 -3
  65. package/src/simulation/timing/lapTelemetry.js +115 -36
  66. package/src/simulation/timing/raceTiming.js +5 -1
  67. package/src/simulation/timing/sectorPerformance.js +104 -26
  68. package/src/simulation/timing/timingHistory.js +229 -14
  69. package/src/simulation/timing/timingLines.js +65 -7
  70. package/src/simulation/track/pitLaneLayout.js +23 -0
  71. package/src/simulation/track/pitLaneState.js +218 -19
  72. package/src/simulation/track/spatialQueries.js +67 -11
  73. package/src/simulation/track/trackMath.js +91 -24
  74. package/src/simulation/track/trackModel.js +2 -0
  75. package/src/simulation/track/trackQueryIndex.js +1334 -100
  76. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  77. package/src/simulation/track/trackQueryScratch.js +7 -0
  78. package/src/simulation/track/trackQueryStats.js +25 -0
  79. package/src/simulation/track/trackSnapshotJson.js +139 -0
  80. package/src/simulation/track/trackStatePolicy.js +249 -1
  81. package/src/simulation/vehicle/contactResolution.js +96 -59
  82. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  83. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  84. package/src/simulation/vehicle/runoffResponse.js +54 -29
  85. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  86. package/src/simulation/vehicle/vehiclePhysics.js +42 -27
  87. package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
  88. package/src/simulation/vehicle/vehicleState.js +6 -0
  89. package/src/simulation/vehicle/wheelSurface.js +807 -92
  90. package/src/styles.css +497 -497
@@ -0,0 +1,1171 @@
1
+ import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
2
+ import { nearestTrackState } from '../../simulation/track/trackModel.js';
3
+ import { nearestPitLaneState } from '../../simulation/track/pitLaneState.js';
4
+ import { resolveTrackStatePitOverride, sampleIndexAtDistance, writeTrackState } from '../../simulation/track/spatialQueries.js';
5
+ import { pointInsideBounds } from '../../simulation/track/trackMath.js';
6
+ import { querySegmentNeighborhoodProjection, querySegmentNeighborhoodProjections } from '../../simulation/track/trackQueryIndex.js';
7
+ import {
8
+ ANALYTIC_TRACK_RAY_MAX_CURVATURE,
9
+ DRIVER_RAY_REFINE_STEPS,
10
+ PIT_CONNECTOR_RAY_FALLBACK_METERS,
11
+ TRACK_RAY_REFINE_STEPS,
12
+ TRACK_RAY_STEP_METERS,
13
+ } from './rayDefaults.js';
14
+ import { findIndexedRayBoundaryDistances } from './indexedRayBands.js';
15
+ import { createRayChannelFlags, isLegalRaySurface, writeRayChannelFlags } from './rayChannels.js';
16
+ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
17
+
18
+ const BOUNDARY_VALIDATION_EPSILON_METERS = 0.25;
19
+ const LOCAL_SEGMENT_RECOVERY_BATCH_SIZE = 16;
20
+
21
+ export function traceIndexedRayBands({
22
+ track,
23
+ origin,
24
+ vector,
25
+ lengthMeters,
26
+ originState,
27
+ channels = [],
28
+ precision = 'driver',
29
+ sharedRayQuery = null,
30
+ stats = null,
31
+ validateBoundaries = true,
32
+ allowPitOverride = false,
33
+ mainTrackOnly = false,
34
+ } = {}) {
35
+ const channelSet = rayChannelFlagsFor(channels, sharedRayQuery, stats);
36
+ const result = prepareTraceResult(sharedRayQuery, lengthMeters);
37
+ const fallbackReason = directTraceFallbackReason({ track, origin, vector, lengthMeters, originState, precision, mainTrackOnly });
38
+ if (fallbackReason) {
39
+ const sampled = trySampledRecovery({
40
+ track,
41
+ origin,
42
+ vector,
43
+ lengthMeters,
44
+ originState,
45
+ channels: channelSet,
46
+ allowPitOverride,
47
+ sharedRayQuery,
48
+ reason: fallbackReason,
49
+ mainTrackOnly,
50
+ precision,
51
+ });
52
+ if (sampled) return sampled;
53
+ return writeTraceMeta(result, false, null, null, fallbackReason);
54
+ }
55
+
56
+ if (mainTrackOnly) {
57
+ const directLocalBand = tryDirectLocalBandRecovery({
58
+ track,
59
+ vector,
60
+ lengthMeters,
61
+ originState,
62
+ channels: channelSet,
63
+ sharedRayQuery,
64
+ reason: 'main-track-only',
65
+ });
66
+ if (directLocalBand) return directLocalBand;
67
+ }
68
+
69
+ const trackHalfWidth = track.width / 2;
70
+ const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
71
+ const boundaries = findIndexedSurfaceBoundaries(
72
+ track,
73
+ origin,
74
+ vector,
75
+ lengthMeters,
76
+ channelSet,
77
+ originState,
78
+ sharedRayQuery,
79
+ );
80
+ if (!boundaries.available) {
81
+ return writeTraceMeta(result, false, null, null, 'boundary-index-unavailable');
82
+ }
83
+ const boundaryStateCache = prepareBoundaryStateCache(sharedRayQuery);
84
+ const boundaryFallbackReason = directBoundaryFallbackReason({
85
+ track,
86
+ origin,
87
+ originState,
88
+ vector,
89
+ lengthMeters,
90
+ channels: channelSet,
91
+ boundaries,
92
+ validateBoundaries,
93
+ boundaryStateCache,
94
+ allowPitOverride,
95
+ });
96
+ if (boundaryFallbackReason) {
97
+ const sampled = trySampledRecovery({
98
+ track,
99
+ origin,
100
+ vector,
101
+ lengthMeters,
102
+ originState,
103
+ channels: channelSet,
104
+ allowPitOverride,
105
+ sharedRayQuery,
106
+ reason: boundaryFallbackReason,
107
+ mainTrackOnly,
108
+ precision,
109
+ });
110
+ if (sampled) return sampled;
111
+ return writeTraceMeta(result, false, null, null, boundaryFallbackReason);
112
+ }
113
+
114
+ if (channelSet.hasRoadEdge) {
115
+ directRoadEdgeHit(result.roadEdge, originState, boundaries.trackEdgeDistance, lengthMeters, trackHalfWidth);
116
+ }
117
+ if (channelSet.hasKerb) {
118
+ directKerbHit(result.kerb, originState, boundaries, lengthMeters);
119
+ }
120
+ if (channelSet.hasIllegalSurface) {
121
+ directIllegalSurfaceHit(result.illegalSurface, track, origin, originState, vector, boundaries, lengthMeters, boundaryStateCache);
122
+ }
123
+ return writeTraceMeta(result, true, 'direct', null, null);
124
+ }
125
+
126
+ function prepareTraceResult(sharedRayQuery, lengthMeters) {
127
+ const result = sharedRayQuery
128
+ ? (sharedRayQuery.traceResult ?? {})
129
+ : {};
130
+ if (sharedRayQuery) sharedRayQuery.traceResult = result;
131
+ result.roadEdge = writeTrackMiss(result.roadEdge ?? {}, lengthMeters);
132
+ result.kerb = writeSurfaceMiss(result.kerb ?? {}, lengthMeters);
133
+ result.illegalSurface = writeSurfaceMiss(result.illegalSurface ?? {}, lengthMeters);
134
+ return writeTraceMeta(result, false, null, null, null);
135
+ }
136
+
137
+ function writeTraceMeta(result, available, path, sampledReason, fallbackReason) {
138
+ result.available = available;
139
+ result.path = path;
140
+ result.sampledReason = sampledReason;
141
+ result.fallbackReason = fallbackReason;
142
+ return result;
143
+ }
144
+
145
+ function rayChannelFlagsFor(channels, sharedRayQuery, stats) {
146
+ const target = sharedRayQuery
147
+ ? (sharedRayQuery.channelFlags ?? createRayChannelFlags())
148
+ : createRayChannelFlags();
149
+ const flags = writeRayChannelFlags(target, channels);
150
+ if (sharedRayQuery) sharedRayQuery.channelFlags = flags;
151
+ if (stats && !Number.isFinite(stats.channelSetAllocations)) stats.channelSetAllocations = 0;
152
+ return flags;
153
+ }
154
+
155
+ function findIndexedSurfaceBoundaries(track, origin, vector, lengthMeters, channels, originState, cache = null) {
156
+ if (cache?.surfaceBoundaries) return cache.surfaceBoundaries;
157
+ const trackHalfWidth = track.width / 2;
158
+ const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
159
+ const runoffOuter = kerbOuter + (track.gravelWidth ?? 0) + (track.runoffWidth ?? 0);
160
+ const scratch = prepareSurfaceBoundaryScratch(cache);
161
+ const offsets = scratch?.offsets ?? [];
162
+ offsets.length = 0;
163
+ let trackPositive = -1;
164
+ let trackNegative = -1;
165
+ let kerbPositive = -1;
166
+ let kerbNegative = -1;
167
+ let runoffPositive = -1;
168
+ let runoffNegative = -1;
169
+
170
+ if (channels.hasRoadEdge || channels.hasKerb) {
171
+ trackPositive = offsets.push(trackHalfWidth) - 1;
172
+ trackNegative = offsets.push(-trackHalfWidth) - 1;
173
+ }
174
+ if (
175
+ channels.hasKerb ||
176
+ (channels.hasIllegalSurface && isLegalRaySurface(originState.surface))
177
+ ) {
178
+ kerbPositive = offsets.push(kerbOuter) - 1;
179
+ kerbNegative = offsets.push(-kerbOuter) - 1;
180
+ }
181
+ if (channels.hasIllegalSurface && originState.surface === 'barrier') {
182
+ runoffPositive = offsets.push(runoffOuter) - 1;
183
+ runoffNegative = offsets.push(-runoffOuter) - 1;
184
+ }
185
+
186
+ const boundaries = findIndexedRayBoundaryDistances(
187
+ track,
188
+ origin,
189
+ vector,
190
+ lengthMeters,
191
+ offsets,
192
+ scratch?.boundaryDistances,
193
+ );
194
+ const result = scratch?.result ?? {};
195
+ result.available = boundaries.available;
196
+ result.trackEdgeDistance = nearestBoundaryDistance(
197
+ boundaries.distances[trackPositive],
198
+ boundaries.segmentIds[trackPositive],
199
+ boundaries.distances[trackNegative],
200
+ boundaries.segmentIds[trackNegative],
201
+ );
202
+ result.trackEdgeSegmentId = nearestBoundarySegmentId(
203
+ boundaries.distances[trackPositive],
204
+ boundaries.segmentIds[trackPositive],
205
+ boundaries.distances[trackNegative],
206
+ boundaries.segmentIds[trackNegative],
207
+ );
208
+ result.kerbOuterDistance = nearestBoundaryDistance(
209
+ boundaries.distances[kerbPositive],
210
+ boundaries.segmentIds[kerbPositive],
211
+ boundaries.distances[kerbNegative],
212
+ boundaries.segmentIds[kerbNegative],
213
+ );
214
+ result.kerbOuterSegmentId = nearestBoundarySegmentId(
215
+ boundaries.distances[kerbPositive],
216
+ boundaries.segmentIds[kerbPositive],
217
+ boundaries.distances[kerbNegative],
218
+ boundaries.segmentIds[kerbNegative],
219
+ );
220
+ result.runoffOuterDistance = nearestBoundaryDistance(
221
+ boundaries.distances[runoffPositive],
222
+ boundaries.segmentIds[runoffPositive],
223
+ boundaries.distances[runoffNegative],
224
+ boundaries.segmentIds[runoffNegative],
225
+ );
226
+ result.runoffOuterSegmentId = nearestBoundarySegmentId(
227
+ boundaries.distances[runoffPositive],
228
+ boundaries.segmentIds[runoffPositive],
229
+ boundaries.distances[runoffNegative],
230
+ boundaries.segmentIds[runoffNegative],
231
+ );
232
+ if (cache) cache.surfaceBoundaries = result;
233
+ return result;
234
+ }
235
+
236
+ function prepareSurfaceBoundaryScratch(cache) {
237
+ if (!cache) return null;
238
+ const scratch = cache.surfaceBoundaryScratch ?? {
239
+ offsets: [],
240
+ boundaryDistances: {},
241
+ result: {},
242
+ };
243
+ cache.surfaceBoundaryScratch = scratch;
244
+ return scratch;
245
+ }
246
+
247
+ function directBoundaryFallbackReason({
248
+ track,
249
+ origin,
250
+ originState,
251
+ vector,
252
+ lengthMeters,
253
+ channels,
254
+ boundaries,
255
+ validateBoundaries,
256
+ boundaryStateCache,
257
+ allowPitOverride,
258
+ }) {
259
+ const trackHalfWidth = track.width / 2;
260
+ const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
261
+ const trustTrackOriginIllegalSurfaceBoundary = originState.surface === 'track';
262
+ const barrierOriginCanStayDirect = originState.surface === 'barrier';
263
+ if (
264
+ validateBoundaries &&
265
+ channels.hasRoadEdge &&
266
+ Number.isFinite(boundaries.trackEdgeDistance) &&
267
+ !validatedTrackBoundaryHit(track, origin, originState, vector, boundaries.trackEdgeDistance, boundaries.trackEdgeSegmentId, lengthMeters, boundaryStateCache, allowPitOverride)
268
+ ) {
269
+ return 'road-edge-validation';
270
+ }
271
+ const kerbBoundaryDistance = minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance);
272
+ const kerbBoundarySegmentId = nearestBoundarySegmentId(
273
+ boundaries.trackEdgeDistance,
274
+ boundaries.trackEdgeSegmentId,
275
+ boundaries.kerbOuterDistance,
276
+ boundaries.kerbOuterSegmentId,
277
+ );
278
+ if (
279
+ validateBoundaries &&
280
+ channels.hasKerb &&
281
+ Number.isFinite(kerbBoundaryDistance) &&
282
+ originState.surface !== 'kerb' &&
283
+ !validatedSurfaceBoundaryHit(track, origin, originState, vector, kerbBoundaryDistance, kerbBoundarySegmentId, lengthMeters, 'kerb', boundaryStateCache, allowPitOverride)
284
+ ) {
285
+ return 'kerb-validation';
286
+ }
287
+ if (
288
+ validateBoundaries &&
289
+ channels.hasIllegalSurface &&
290
+ !trustTrackOriginIllegalSurfaceBoundary &&
291
+ isLegalRaySurface(originState.surface) &&
292
+ Number.isFinite(boundaries.kerbOuterDistance) &&
293
+ !validatedSurfaceBoundaryHit(track, origin, originState, vector, boundaries.kerbOuterDistance, boundaries.kerbOuterSegmentId, lengthMeters, 'illegalSurface', boundaryStateCache, allowPitOverride)
294
+ ) {
295
+ return 'illegal-surface-validation';
296
+ }
297
+ if (
298
+ validateBoundaries &&
299
+ channels.hasIllegalSurface &&
300
+ !trustTrackOriginIllegalSurfaceBoundary &&
301
+ originState.surface === 'barrier' &&
302
+ Number.isFinite(boundaries.runoffOuterDistance) &&
303
+ !validatedSurfaceBoundaryHit(track, origin, originState, vector, boundaries.runoffOuterDistance, boundaries.runoffOuterSegmentId, lengthMeters, 'illegalSurface', boundaryStateCache, allowPitOverride)
304
+ ) {
305
+ return 'illegal-surface-validation';
306
+ }
307
+ if (
308
+ channels.hasRoadEdge &&
309
+ boundaries.trackEdgeDistance == null &&
310
+ Math.abs(originState.signedOffset) > trackHalfWidth &&
311
+ !barrierOriginCanStayDirect
312
+ ) {
313
+ return 'road-edge-recovery';
314
+ }
315
+ if (
316
+ channels.hasKerb &&
317
+ boundaries.trackEdgeDistance == null &&
318
+ boundaries.kerbOuterDistance == null &&
319
+ Math.abs(originState.signedOffset) > kerbOuter &&
320
+ !barrierOriginCanStayDirect
321
+ ) {
322
+ return 'kerb-recovery';
323
+ }
324
+ if (
325
+ channels.hasIllegalSurface &&
326
+ originState.surface === 'barrier' &&
327
+ boundaries.runoffOuterDistance == null &&
328
+ Math.abs(originState.signedOffset) > kerbOuter &&
329
+ !barrierOriginCanStayDirect
330
+ ) {
331
+ return 'illegal-surface-recovery';
332
+ }
333
+ return null;
334
+ }
335
+
336
+ function validatedTrackBoundaryHit(track, origin, originState, vector, distance, segmentId, lengthMeters, boundaryStateCache, allowPitOverride) {
337
+ if (originState.surface === 'track') {
338
+ const after = stateAtBoundary(track, origin, originState, vector, distance, segmentId, lengthMeters, 1, boundaryStateCache, allowPitOverride);
339
+ return Math.abs(after.crossTrackError ?? Infinity) > track.width / 2;
340
+ }
341
+ const originInside = Math.abs(originState.signedOffset ?? 0) <= track.width / 2;
342
+ const before = stateAtBoundary(track, origin, originState, vector, distance, segmentId, lengthMeters, -1, boundaryStateCache, allowPitOverride);
343
+ const after = stateAtBoundary(track, origin, originState, vector, distance, segmentId, lengthMeters, 1, boundaryStateCache, allowPitOverride);
344
+ const beforeInside = Math.abs(before.crossTrackError ?? Infinity) <= track.width / 2;
345
+ const afterInside = Math.abs(after.crossTrackError ?? Infinity) <= track.width / 2;
346
+ if (beforeInside === afterInside) return false;
347
+ return originInside ? !afterInside : afterInside;
348
+ }
349
+
350
+ function validatedSurfaceBoundaryHit(track, origin, originState, vector, distance, segmentId, lengthMeters, channel, boundaryStateCache, allowPitOverride) {
351
+ if (originState.surface === 'track') {
352
+ const after = stateAtBoundary(track, origin, originState, vector, distance, segmentId, lengthMeters, 1, boundaryStateCache, allowPitOverride);
353
+ return matchesSurfaceChannel(channel, after);
354
+ }
355
+ const before = stateAtBoundary(track, origin, originState, vector, distance, segmentId, lengthMeters, -1, boundaryStateCache, allowPitOverride);
356
+ const after = stateAtBoundary(track, origin, originState, vector, distance, segmentId, lengthMeters, 1, boundaryStateCache, allowPitOverride);
357
+ return matchesSurfaceChannel(channel, before) || matchesSurfaceChannel(channel, after);
358
+ }
359
+
360
+ function prepareBoundaryStateCache(sharedRayQuery) {
361
+ if (!sharedRayQuery) return null;
362
+ const cache = sharedRayQuery.boundaryStateCache ?? {
363
+ distances: [],
364
+ segmentIds: [],
365
+ directions: [],
366
+ states: [],
367
+ };
368
+ sharedRayQuery.boundaryStateCache = cache;
369
+ cache.distances.length = 0;
370
+ cache.segmentIds.length = 0;
371
+ cache.directions.length = 0;
372
+ cache.states.length = 0;
373
+ return cache;
374
+ }
375
+
376
+ function stateAtBoundary(track, origin, originState, vector, distance, segmentId, lengthMeters, direction, boundaryStateCache = null, allowPitOverride = false) {
377
+ if (boundaryStateCache) {
378
+ for (let index = 0; index < boundaryStateCache.distances.length; index += 1) {
379
+ if (
380
+ boundaryStateCache.distances[index] === distance &&
381
+ boundaryStateCache.segmentIds[index] === segmentId &&
382
+ boundaryStateCache.directions[index] === direction
383
+ ) {
384
+ return boundaryStateCache.states[index];
385
+ }
386
+ }
387
+ }
388
+ const epsilon = metersToSimUnits(BOUNDARY_VALIDATION_EPSILON_METERS);
389
+ const maxDistance = metersToSimUnits(lengthMeters);
390
+ const sampleDistance = Math.max(0, Math.min(maxDistance, distance + direction * epsilon));
391
+ const point = {
392
+ x: origin.x + vector.x * sampleDistance,
393
+ y: origin.y + vector.y * sampleDistance,
394
+ };
395
+ const state = indexedBoundaryTrackState(track, point, originState, segmentId, allowPitOverride);
396
+ if (boundaryStateCache) {
397
+ boundaryStateCache.distances.push(distance);
398
+ boundaryStateCache.segmentIds.push(segmentId);
399
+ boundaryStateCache.directions.push(direction);
400
+ boundaryStateCache.states.push(state);
401
+ }
402
+ return state;
403
+ }
404
+
405
+ function indexedBoundaryTrackState(track, point, originState, segmentId, allowPitOverride) {
406
+ const progressHint = originState?.distance;
407
+ if (!canUseIndexedBoundaryProjection(track, point, originState, segmentId)) {
408
+ return nearestTrackState(track, point, progressHint, { allowPitOverride });
409
+ }
410
+ return projectedTrackStateFromSegmentSeed(track, point, progressHint, segmentId, allowPitOverride) ??
411
+ nearestTrackState(track, point, progressHint, { allowPitOverride });
412
+ }
413
+
414
+ function canUseIndexedBoundaryProjection(track, point, originState, segmentId) {
415
+ return point != null &&
416
+ Number.isFinite(originState?.distance) &&
417
+ Number.isInteger(segmentId) &&
418
+ !originState?.inPitLane &&
419
+ !isNearPitConnector(track, originState) &&
420
+ (
421
+ originState.surface !== 'kerb' ||
422
+ Math.abs(originState.curvature ?? 0) <= ANALYTIC_TRACK_RAY_MAX_CURVATURE
423
+ ) &&
424
+ canUseIndexedRecoveryRayApproximation(track, originState);
425
+ }
426
+
427
+ function projectedTrackStateFromSegmentSeed(
428
+ track,
429
+ point,
430
+ progressHint,
431
+ segmentId,
432
+ allowPitOverride,
433
+ cache = null,
434
+ ) {
435
+ if (
436
+ point == null ||
437
+ !Number.isFinite(progressHint) ||
438
+ !Number.isInteger(segmentId)
439
+ ) {
440
+ return null;
441
+ }
442
+ const projection = querySegmentNeighborhoodProjection(track, point, segmentId, {
443
+ radius: 2,
444
+ preferredDistance: progressHint,
445
+ target: cache?.projection ?? null,
446
+ });
447
+ if (!projection) return null;
448
+
449
+ const state = writeTrackState(cache?.state ?? {}, track, point, projection);
450
+ if (!allowPitOverride) return state;
451
+
452
+ const pitLane = track?.pitLane;
453
+ if (!pitLane?.enabled || !pointInsideBounds(point, pitLane.bounds)) return state;
454
+ const pitState = nearestPitLaneState(track, point, progressHint);
455
+ if (!pitState) return state;
456
+ return resolveTrackStatePitOverride(track, state, pitState);
457
+ }
458
+
459
+ function matchesSurfaceChannel(channel, state) {
460
+ if (channel === 'kerb') return state?.surface === 'kerb';
461
+ if (channel === 'illegalSurface') return !isLegalRaySurface(state?.surface) && state?.surface !== 'barrier';
462
+ return false;
463
+ }
464
+
465
+ function directTraceFallbackReason({ track, origin, vector, lengthMeters, originState, precision, mainTrackOnly }) {
466
+ if (precision !== 'driver' && precision !== 'debug') return 'precision';
467
+ if (!track?.queryIndex?.centerline?.segmentCount) return 'missing-index';
468
+ if (!Array.isArray(track.samples) || track.samples.length === 0) return 'missing-samples';
469
+ if (!finitePoint(origin) || !finiteVector(vector) || !Number.isFinite(lengthMeters) || lengthMeters < 0) return 'invalid-ray';
470
+ if (!originState || !Number.isFinite(originState.signedOffset) || !Number.isFinite(originState.distance)) return 'invalid-origin-state';
471
+ if (originState.inPitLane) return 'pit-lane';
472
+ if (!mainTrackOnly && isNearPitConnector(track, originState)) return 'pit-connector';
473
+ return null;
474
+ }
475
+
476
+ function directRoadEdgeHit(target, originState, distance, lengthMeters, trackHalfWidth) {
477
+ if (!Number.isFinite(distance)) return writeTrackMiss(target, lengthMeters);
478
+ target.hit = true;
479
+ target.distanceMeters = simUnitsToMeters(distance);
480
+ target.kind = Math.abs(originState.signedOffset ?? 0) <= trackHalfWidth ? 'exit' : 'entry';
481
+ return target;
482
+ }
483
+
484
+ function directKerbHit(target, originState, boundaries, lengthMeters) {
485
+ if (originState.surface === 'kerb') {
486
+ return writeSurfaceHit(target, 0, 'kerb');
487
+ }
488
+ const distance = minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance);
489
+ if (!Number.isFinite(distance)) return writeSurfaceMiss(target, lengthMeters);
490
+ return writeSurfaceHit(target, simUnitsToMeters(distance), 'kerb');
491
+ }
492
+
493
+ function directIllegalSurfaceHit(target, track, origin, originState, vector, boundaries, lengthMeters, boundaryStateCache) {
494
+ if (!isLegalRaySurface(originState.surface) && originState.surface !== 'barrier') {
495
+ return writeSurfaceHit(target, 0, originState.surface ?? null);
496
+ }
497
+ if (originState.surface === 'barrier') {
498
+ if (!Number.isFinite(boundaries.runoffOuterDistance)) return writeSurfaceMiss(target, lengthMeters);
499
+ return writeSurfaceHit(
500
+ target,
501
+ simUnitsToMeters(boundaries.runoffOuterDistance),
502
+ surfaceAtBoundaryHit(
503
+ track,
504
+ origin,
505
+ originState,
506
+ vector,
507
+ boundaries.runoffOuterDistance,
508
+ boundaries.runoffOuterSegmentId,
509
+ lengthMeters,
510
+ 'illegalSurface',
511
+ boundaryStateCache,
512
+ ),
513
+ );
514
+ }
515
+ const distance = boundaries.kerbOuterDistance;
516
+ if (!Number.isFinite(distance)) return writeSurfaceMiss(target, lengthMeters);
517
+ const surface = originState.surface === 'track'
518
+ ? tracerOwnedIllegalSurface(track)
519
+ : null;
520
+ return writeSurfaceHit(
521
+ target,
522
+ simUnitsToMeters(distance),
523
+ surface ?? surfaceAtBoundaryHit(
524
+ track,
525
+ origin,
526
+ originState,
527
+ vector,
528
+ distance,
529
+ boundaries.kerbOuterSegmentId,
530
+ lengthMeters,
531
+ 'illegalSurface',
532
+ boundaryStateCache,
533
+ ),
534
+ );
535
+ }
536
+
537
+ function tracerOwnedIllegalSurface(track) {
538
+ if ((track?.gravelWidth ?? 0) > 0) return 'gravel';
539
+ if ((track?.runoffWidth ?? 0) > 0) return 'grass';
540
+ return null;
541
+ }
542
+
543
+ function surfaceAtBoundaryHit(track, origin, originState, vector, distance, segmentId, lengthMeters, channel, boundaryStateCache) {
544
+ const after = stateAtBoundary(track, origin, originState, vector, distance, segmentId, lengthMeters, 1, boundaryStateCache);
545
+ if (matchesSurfaceChannel(channel, after)) return after.surface ?? null;
546
+ const before = stateAtBoundary(track, origin, originState, vector, distance, segmentId, lengthMeters, -1, boundaryStateCache);
547
+ if (matchesSurfaceChannel(channel, before)) return before.surface ?? null;
548
+ return null;
549
+ }
550
+
551
+ function trySampledRecovery({
552
+ track,
553
+ origin,
554
+ vector,
555
+ lengthMeters,
556
+ originState,
557
+ channels,
558
+ allowPitOverride,
559
+ sharedRayQuery,
560
+ reason,
561
+ mainTrackOnly,
562
+ precision,
563
+ }) {
564
+ if (!canUseSampledRecovery(reason)) return null;
565
+ const directPitLane = tryDirectPitLaneRecovery({
566
+ vector,
567
+ lengthMeters,
568
+ originState,
569
+ channels,
570
+ sharedRayQuery,
571
+ reason,
572
+ });
573
+ if (directPitLane) return directPitLane;
574
+ if (mainTrackOnly || canUseDirectLocalBandRecovery(track, originState, reason)) {
575
+ const directLocalBand = tryDirectLocalBandRecovery({
576
+ track,
577
+ vector,
578
+ lengthMeters,
579
+ originState,
580
+ channels,
581
+ sharedRayQuery,
582
+ reason,
583
+ });
584
+ if (directLocalBand) return directLocalBand;
585
+ }
586
+ return sampleRayBands({
587
+ track,
588
+ origin,
589
+ vector,
590
+ lengthMeters,
591
+ originState,
592
+ channels,
593
+ allowPitOverride,
594
+ sharedRayQuery,
595
+ reason,
596
+ mainTrackOnly,
597
+ precision,
598
+ });
599
+ }
600
+
601
+ function tryDirectPitLaneRecovery({ vector, lengthMeters, originState, channels, sharedRayQuery, reason }) {
602
+ if (reason !== 'pit-lane' || !originState?.inPitLane) return null;
603
+ const signedOffset = originState.pitLaneSignedOffset ?? originState.signedOffset;
604
+ const roadWidth = originState.pitLaneRoadWidth;
605
+ if (
606
+ !Number.isFinite(signedOffset) ||
607
+ !Number.isFinite(roadWidth) ||
608
+ roadWidth <= 0 ||
609
+ !Number.isFinite(originState.normalX) ||
610
+ !Number.isFinite(originState.normalY)
611
+ ) {
612
+ return null;
613
+ }
614
+ const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
615
+ if (Math.abs(lateral) < 0.05) return null;
616
+ const halfWidth = roadWidth / 2;
617
+ if (Math.abs(signedOffset) > halfWidth + 1e-6) return null;
618
+ const targetOffset = lateral > 0 ? halfWidth : -halfWidth;
619
+ const boundaryDistance = (targetOffset - signedOffset) / lateral;
620
+ if (!Number.isFinite(boundaryDistance) || boundaryDistance < 0) return null;
621
+ const length = metersToSimUnits(lengthMeters);
622
+ const result = prepareTraceResult(sharedRayQuery, lengthMeters);
623
+ const roadEdge = result.roadEdge;
624
+ if (channels.hasRoadEdge && boundaryDistance <= length) {
625
+ roadEdge.hit = true;
626
+ roadEdge.distanceMeters = simUnitsToMeters(boundaryDistance);
627
+ roadEdge.kind = 'exit';
628
+ }
629
+ return writeTraceMeta(result, true, 'direct', null, null);
630
+ }
631
+
632
+ function canUseSampledRecovery(reason) {
633
+ return reason === 'pit-lane' ||
634
+ reason === 'pit-connector' ||
635
+ reason === 'road-edge-recovery' ||
636
+ reason === 'kerb-recovery' ||
637
+ reason === 'illegal-surface-recovery' ||
638
+ reason === 'road-edge-validation' ||
639
+ reason === 'kerb-validation' ||
640
+ reason === 'illegal-surface-validation';
641
+ }
642
+
643
+ function canUseDirectLocalBandRecovery(track, originState, reason) {
644
+ if (
645
+ reason !== 'road-edge-recovery' &&
646
+ reason !== 'kerb-recovery' &&
647
+ reason !== 'illegal-surface-recovery'
648
+ ) {
649
+ return false;
650
+ }
651
+ if (originState?.inPitLane || isNearPitConnector(track, originState)) return false;
652
+ return canUseIndexedRecoveryRayApproximation(track, originState);
653
+ }
654
+
655
+ function tryDirectLocalBandRecovery({ track, vector, lengthMeters, originState, channels, sharedRayQuery, reason }) {
656
+ if (
657
+ reason !== 'road-edge-recovery' &&
658
+ reason !== 'kerb-recovery' &&
659
+ reason !== 'illegal-surface-recovery' &&
660
+ reason !== 'main-track-only'
661
+ ) {
662
+ return null;
663
+ }
664
+ if (
665
+ originState?.inPitLane ||
666
+ !Number.isFinite(originState?.signedOffset) ||
667
+ !Number.isFinite(originState?.normalX) ||
668
+ !Number.isFinite(originState?.normalY) ||
669
+ !canUseIndexedRecoveryRayApproximation(track, originState) ||
670
+ Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE
671
+ ) {
672
+ return null;
673
+ }
674
+ const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
675
+ if (Math.abs(lateral) < 0.05) return null;
676
+
677
+ const maxDistance = metersToSimUnits(lengthMeters);
678
+ const trackHalfWidth = track.width / 2;
679
+ const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
680
+ const offset = originState.signedOffset;
681
+ const result = prepareTraceResult(sharedRayQuery, lengthMeters);
682
+
683
+ if (channels.hasRoadEdge) {
684
+ const distance = distanceToAbsBoundary(offset, lateral, trackHalfWidth, maxDistance);
685
+ if (distance != null) {
686
+ result.roadEdge.hit = true;
687
+ result.roadEdge.distanceMeters = simUnitsToMeters(distance);
688
+ result.roadEdge.kind = Math.abs(offset) <= trackHalfWidth ? 'exit' : 'entry';
689
+ } else if (
690
+ Math.abs(offset) > trackHalfWidth &&
691
+ !canUseDirectOutwardRoadEdgeMiss(originState, reason)
692
+ ) {
693
+ return null;
694
+ }
695
+ }
696
+
697
+ if (channels.hasKerb) {
698
+ const kerbDistance = distanceToAbsBand(offset, lateral, trackHalfWidth, kerbOuter, maxDistance);
699
+ if (kerbDistance != null) writeSurfaceHit(result.kerb, simUnitsToMeters(kerbDistance), 'kerb');
700
+ }
701
+
702
+ if (channels.hasIllegalSurface) {
703
+ if (matchesSurfaceChannel('illegalSurface', originState)) {
704
+ writeSurfaceHit(result.illegalSurface, 0, originState.surface ?? null);
705
+ } else {
706
+ const illegalDistance = distanceToIllegalSurface(offset, lateral, kerbOuter, maxDistance);
707
+ if (illegalDistance != null) {
708
+ writeSurfaceHit(result.illegalSurface, simUnitsToMeters(illegalDistance), tracerOwnedIllegalSurface(track));
709
+ }
710
+ }
711
+ }
712
+
713
+ return writeTraceMeta(result, true, 'direct', null, null);
714
+ }
715
+
716
+ function distanceToAbsBand(offset, lateral, minAbsOffset, maxAbsOffset, maxDistance) {
717
+ const currentAbs = Math.abs(offset);
718
+ if (currentAbs >= minAbsOffset && currentAbs <= maxAbsOffset) return 0;
719
+ const enteringBoundary = currentAbs < minAbsOffset ? minAbsOffset : maxAbsOffset;
720
+ return distanceToAbsBoundary(offset, lateral, enteringBoundary, maxDistance);
721
+ }
722
+
723
+ function canUseDirectOutwardRoadEdgeMiss(originState, reason) {
724
+ return reason !== 'main-track-only' &&
725
+ Math.abs(originState?.curvature ?? 0) <= 1e-9;
726
+ }
727
+
728
+ function distanceToIllegalSurface(offset, lateral, kerbOuter, maxDistance) {
729
+ const currentAbs = Math.abs(offset);
730
+ if (currentAbs > kerbOuter) return 0;
731
+ return distanceToAbsBoundary(offset, lateral, kerbOuter, maxDistance);
732
+ }
733
+
734
+ function distanceToAbsBoundary(offset, lateral, targetAbsOffset, maxDistance) {
735
+ let best = Infinity;
736
+ const positive = distanceToOffset(targetAbsOffset, offset, lateral);
737
+ const negative = distanceToOffset(-targetAbsOffset, offset, lateral);
738
+ if (Number.isFinite(positive) && positive >= 0 && positive <= maxDistance) best = positive;
739
+ if (Number.isFinite(negative) && negative >= 0 && negative <= maxDistance && negative < best) best = negative;
740
+ return Number.isFinite(best) ? best : null;
741
+ }
742
+
743
+ function distanceToOffset(targetOffset, offset, lateral) {
744
+ return (targetOffset - offset) / lateral;
745
+ }
746
+
747
+ function sampleRayBands({
748
+ track,
749
+ origin,
750
+ vector,
751
+ lengthMeters,
752
+ originState,
753
+ channels,
754
+ allowPitOverride,
755
+ sharedRayQuery,
756
+ reason,
757
+ mainTrackOnly = false,
758
+ precision = 'driver',
759
+ }) {
760
+ const result = prepareTraceResult(sharedRayQuery, lengthMeters);
761
+ const roadEdge = result.roadEdge;
762
+ const kerb = result.kerb;
763
+ const illegalSurface = result.illegalSurface;
764
+ let pendingKerb = false;
765
+ let pendingIllegalSurface = false;
766
+ let pendingSurfaceCount = 0;
767
+ if (channels.hasKerb) {
768
+ if (matchesSurfaceChannel('kerb', originState)) {
769
+ writeSurfaceHit(kerb, 0, originState.surface ?? null);
770
+ } else {
771
+ pendingKerb = true;
772
+ pendingSurfaceCount += 1;
773
+ }
774
+ }
775
+ if (channels.hasIllegalSurface) {
776
+ if (matchesSurfaceChannel('illegalSurface', originState)) {
777
+ writeSurfaceHit(illegalSurface, 0, originState.surface ?? null);
778
+ } else {
779
+ pendingIllegalSurface = true;
780
+ pendingSurfaceCount += 1;
781
+ }
782
+ }
783
+
784
+ const maxDistance = metersToSimUnits(lengthMeters);
785
+ const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
786
+ const sampleCache = prepareSampleStateCache(sharedRayQuery);
787
+ const includePitLane = Boolean(originState.inPitLane);
788
+ const needsRoadEdge = channels.hasRoadEdge;
789
+ const localSeedSegmentId = mainTrackOnly && Number.isFinite(originState?.distance)
790
+ ? (Number.isInteger(originState?.segmentId)
791
+ ? originState.segmentId
792
+ : sampleIndexAtDistance(track, originState.distance))
793
+ : (Number.isInteger(originState?.segmentId) ? originState.segmentId : null);
794
+ const useLocalSeedTracking = Number.isInteger(localSeedSegmentId) &&
795
+ canTrackSampledRecoveryFromLocalSegment(track, originState, vector, reason, mainTrackOnly);
796
+ let roadEdgeResolved = !needsRoadEdge;
797
+ let previousDistance = 0;
798
+ let previousState = originState;
799
+ let previousInside = isInsideTrackBorder(originState, track, includePitLane);
800
+
801
+ for (let distance = 0; distance <= maxDistance;) {
802
+ const localBatch = distance > 0 && useLocalSeedTracking
803
+ ? stateBatchAtSamples(
804
+ track,
805
+ origin,
806
+ vector,
807
+ distance,
808
+ maxDistance,
809
+ step,
810
+ originState.distance,
811
+ previousState,
812
+ localSeedSegmentId,
813
+ allowPitOverride,
814
+ sampleCache,
815
+ )
816
+ : null;
817
+ const stateCount = localBatch?.count ?? 1;
818
+
819
+ for (let stateIndex = 0; stateIndex < stateCount; stateIndex += 1) {
820
+ const currentDistance = distance + step * stateIndex;
821
+ const state = currentDistance === 0
822
+ ? originState
823
+ : localBatch?.states?.[stateIndex] ?? stateAtSample(
824
+ track,
825
+ origin,
826
+ vector,
827
+ currentDistance,
828
+ originState.distance,
829
+ previousState,
830
+ useLocalSeedTracking,
831
+ localSeedSegmentId,
832
+ allowPitOverride,
833
+ sampleCache,
834
+ );
835
+
836
+ if (needsRoadEdge && !roadEdgeResolved) {
837
+ const inside = isInsideTrackBorder(state, track, includePitLane);
838
+ if (currentDistance > 0 && inside !== previousInside) {
839
+ roadEdge.hit = true;
840
+ roadEdge.distanceMeters = simUnitsToMeters(refineSampledTransition({
841
+ track,
842
+ origin,
843
+ vector,
844
+ progressHint: originState.distance,
845
+ low: previousDistance,
846
+ high: currentDistance,
847
+ previousMatches: previousInside,
848
+ matches: (candidate) => isInsideTrackBorder(candidate, track, includePitLane),
849
+ allowPitOverride,
850
+ useLocalSeedTracking,
851
+ seedSegmentId: sampledTransitionSegmentId(previousState, state, localSeedSegmentId),
852
+ sampleCache,
853
+ precision,
854
+ }));
855
+ roadEdge.kind = previousInside ? 'exit' : 'entry';
856
+ roadEdgeResolved = true;
857
+ }
858
+ previousInside = inside;
859
+ }
860
+
861
+ if (pendingKerb && matchesSurfaceChannel('kerb', state)) {
862
+ const hitDistance = previousState
863
+ ? refineSampledTransition({
864
+ track,
865
+ origin,
866
+ vector,
867
+ progressHint: originState.distance,
868
+ low: previousDistance,
869
+ high: currentDistance,
870
+ previousMatches: false,
871
+ matches: (candidate) => matchesSurfaceChannel('kerb', candidate),
872
+ allowPitOverride,
873
+ useLocalSeedTracking,
874
+ seedSegmentId: sampledTransitionSegmentId(previousState, state, localSeedSegmentId),
875
+ sampleCache,
876
+ precision,
877
+ })
878
+ : currentDistance;
879
+ writeSurfaceHit(kerb, simUnitsToMeters(hitDistance), state.surface ?? null);
880
+ pendingKerb = false;
881
+ pendingSurfaceCount -= 1;
882
+ }
883
+
884
+ if (pendingIllegalSurface && matchesSurfaceChannel('illegalSurface', state)) {
885
+ const hitDistance = previousState
886
+ ? refineSampledTransition({
887
+ track,
888
+ origin,
889
+ vector,
890
+ progressHint: originState.distance,
891
+ low: previousDistance,
892
+ high: currentDistance,
893
+ previousMatches: false,
894
+ matches: (candidate) => matchesSurfaceChannel('illegalSurface', candidate),
895
+ allowPitOverride,
896
+ useLocalSeedTracking,
897
+ seedSegmentId: sampledTransitionSegmentId(previousState, state, localSeedSegmentId),
898
+ sampleCache,
899
+ precision,
900
+ })
901
+ : currentDistance;
902
+ writeSurfaceHit(illegalSurface, simUnitsToMeters(hitDistance), state.surface ?? null);
903
+ pendingIllegalSurface = false;
904
+ pendingSurfaceCount -= 1;
905
+ }
906
+
907
+ if (roadEdgeResolved && pendingSurfaceCount === 0) {
908
+ distance = maxDistance + step;
909
+ break;
910
+ }
911
+ previousDistance = currentDistance;
912
+ previousState = state;
913
+ }
914
+
915
+ distance += step * stateCount;
916
+ }
917
+
918
+ return writeTraceMeta(result, true, 'sampled', reason, null);
919
+ }
920
+
921
+ function prepareSampleStateCache(sharedRayQuery) {
922
+ if (!sharedRayQuery) return null;
923
+ const cache = sharedRayQuery.sampleStateCache ?? {
924
+ point: {},
925
+ projection: {},
926
+ state: {},
927
+ batchPoints: [],
928
+ batchProjections: [],
929
+ batchStates: [],
930
+ };
931
+ sharedRayQuery.sampleStateCache = cache;
932
+ return cache;
933
+ }
934
+
935
+ function canTrackSampledRecoveryFromLocalSegment(track, originState, vector, reason, mainTrackOnly) {
936
+ if (!isLocalSegmentTrackableSampleReason(reason)) return false;
937
+ if (originState?.inPitLane) return false;
938
+ if (mainTrackOnly) return true;
939
+ if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return false;
940
+ const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
941
+ if (originState.surface === 'kerb' && Math.abs(lateral) < 0.05) return false;
942
+ if (isNearPitConnector(track, originState)) return false;
943
+ return canUseIndexedRecoveryRayApproximation(track, originState);
944
+ }
945
+
946
+ function isLocalSegmentTrackableSampleReason(reason) {
947
+ return reason === 'road-edge-recovery' ||
948
+ reason === 'kerb-recovery' ||
949
+ reason === 'illegal-surface-recovery' ||
950
+ reason === 'road-edge-validation' ||
951
+ reason === 'kerb-validation' ||
952
+ reason === 'illegal-surface-validation';
953
+ }
954
+
955
+ function sampledTransitionSegmentId(previousState, currentState, fallbackSegmentId) {
956
+ if (Number.isInteger(previousState?.segmentId)) return previousState.segmentId;
957
+ if (Number.isInteger(currentState?.segmentId)) return currentState.segmentId;
958
+ return fallbackSegmentId;
959
+ }
960
+
961
+ function stateBatchAtSamples(
962
+ track,
963
+ origin,
964
+ vector,
965
+ startDistance,
966
+ maxDistance,
967
+ step,
968
+ progressHint,
969
+ seedState,
970
+ seedSegmentId,
971
+ allowPitOverride,
972
+ sampleCache,
973
+ ) {
974
+ const remainingStates = Math.floor((maxDistance - startDistance) / step) + 1;
975
+ const count = Math.max(0, Math.min(LOCAL_SEGMENT_RECOVERY_BATCH_SIZE, remainingStates));
976
+ const batchPoints = sampleCache?.batchPoints ?? [];
977
+ const batchProjections = sampleCache?.batchProjections ?? [];
978
+ const batchStates = sampleCache?.batchStates ?? [];
979
+ batchPoints.length = count;
980
+ batchProjections.length = count;
981
+ batchStates.length = count;
982
+
983
+ for (let index = 0; index < count; index += 1) {
984
+ const point = batchPoints[index] ?? {};
985
+ const distance = startDistance + step * index;
986
+ point.x = origin.x + vector.x * distance;
987
+ point.y = origin.y + vector.y * distance;
988
+ batchPoints[index] = point;
989
+ }
990
+
991
+ const projections = querySegmentNeighborhoodProjections(
992
+ track,
993
+ batchPoints,
994
+ Number.isInteger(seedState?.segmentId) ? seedState.segmentId : seedSegmentId,
995
+ {
996
+ radius: 2,
997
+ preferredDistance: progressHint,
998
+ target: batchProjections,
999
+ },
1000
+ );
1001
+
1002
+ for (let index = 0; index < count; index += 1) {
1003
+ const projection = projections?.[index];
1004
+ if (!projection) {
1005
+ batchStates[index] = nearestTrackState(track, batchPoints[index], progressHint, { allowPitOverride });
1006
+ continue;
1007
+ }
1008
+ batchStates[index] = writeTrackState(batchStates[index] ?? {}, track, batchPoints[index], projection);
1009
+ }
1010
+
1011
+ return {
1012
+ states: batchStates,
1013
+ count,
1014
+ };
1015
+ }
1016
+
1017
+ function stateAtSample(
1018
+ track,
1019
+ origin,
1020
+ vector,
1021
+ distance,
1022
+ progressHint,
1023
+ seedState,
1024
+ useLocalSeedTracking,
1025
+ seedSegmentId,
1026
+ allowPitOverride,
1027
+ sampleCache,
1028
+ ) {
1029
+ const point = sampleCache?.point ?? {};
1030
+ point.x = origin.x + vector.x * distance;
1031
+ point.y = origin.y + vector.y * distance;
1032
+ if (useLocalSeedTracking) {
1033
+ const localState = projectedTrackStateFromSegmentSeed(
1034
+ track,
1035
+ point,
1036
+ progressHint,
1037
+ Number.isInteger(seedState?.segmentId) ? seedState.segmentId : seedSegmentId,
1038
+ allowPitOverride,
1039
+ sampleCache,
1040
+ );
1041
+ if (localState) return localState;
1042
+ }
1043
+ return nearestTrackState(track, point, progressHint, { allowPitOverride });
1044
+ }
1045
+
1046
+ function refineSampledTransition({
1047
+ track,
1048
+ origin,
1049
+ vector,
1050
+ progressHint,
1051
+ low,
1052
+ high,
1053
+ previousMatches,
1054
+ matches,
1055
+ allowPitOverride,
1056
+ useLocalSeedTracking = false,
1057
+ seedSegmentId = null,
1058
+ sampleCache = null,
1059
+ precision = 'driver',
1060
+ }) {
1061
+ const refineSteps = refineStepsForPrecision(precision);
1062
+ for (let index = 0; index < refineSteps; index += 1) {
1063
+ const middle = (low + high) / 2;
1064
+ const point = {
1065
+ x: origin.x + vector.x * middle,
1066
+ y: origin.y + vector.y * middle,
1067
+ };
1068
+ const state = useLocalSeedTracking
1069
+ ? projectedTrackStateFromSegmentSeed(
1070
+ track,
1071
+ point,
1072
+ progressHint,
1073
+ seedSegmentId,
1074
+ allowPitOverride,
1075
+ sampleCache,
1076
+ ) ?? nearestTrackState(track, point, progressHint, { allowPitOverride })
1077
+ : nearestTrackState(track, point, progressHint, { allowPitOverride });
1078
+ if (matches(state) === previousMatches) low = middle;
1079
+ else high = middle;
1080
+ }
1081
+ return high;
1082
+ }
1083
+
1084
+ function refineStepsForPrecision(precision) {
1085
+ return precision === 'debug' ? TRACK_RAY_REFINE_STEPS : DRIVER_RAY_REFINE_STEPS;
1086
+ }
1087
+
1088
+ function isInsideTrackBorder(state, track, includePitLane = false) {
1089
+ if (includePitLane && state?.inPitLane) return true;
1090
+ return Math.abs(state?.crossTrackError ?? Infinity) <= track.width / 2;
1091
+ }
1092
+
1093
+ function createTrackMiss(lengthMeters) {
1094
+ return writeTrackMiss({}, lengthMeters);
1095
+ }
1096
+
1097
+ function createSurfaceMiss(lengthMeters) {
1098
+ return writeSurfaceMiss({}, lengthMeters);
1099
+ }
1100
+
1101
+ function writeTrackMiss(target, lengthMeters) {
1102
+ target.hit = false;
1103
+ target.distanceMeters = lengthMeters;
1104
+ target.kind = null;
1105
+ return target;
1106
+ }
1107
+
1108
+ function writeSurfaceMiss(target, lengthMeters) {
1109
+ target.hit = false;
1110
+ target.distanceMeters = lengthMeters;
1111
+ target.surface = null;
1112
+ return target;
1113
+ }
1114
+
1115
+ function writeSurfaceHit(target, distanceMeters, surface) {
1116
+ target.hit = true;
1117
+ target.distanceMeters = distanceMeters;
1118
+ target.surface = surface ?? null;
1119
+ return target;
1120
+ }
1121
+
1122
+ function isNearPitConnector(track, state) {
1123
+ const pitLane = track.pitLane;
1124
+ if (!pitLane?.enabled || !Number.isFinite(state?.distance)) return false;
1125
+ const window = metersToSimUnits(PIT_CONNECTOR_RAY_FALLBACK_METERS);
1126
+ const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
1127
+ const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
1128
+ return wrappedTrackDistance(state.distance, entryDistance, track.length) <= window ||
1129
+ wrappedTrackDistance(state.distance, exitDistance, track.length) <= window;
1130
+ }
1131
+
1132
+ function wrappedTrackDistance(first, second, totalLength) {
1133
+ if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(totalLength) || totalLength <= 0) {
1134
+ return Infinity;
1135
+ }
1136
+ const delta = Math.abs(first - second);
1137
+ return Math.min(delta, totalLength - delta);
1138
+ }
1139
+
1140
+ function minFinite(...values) {
1141
+ let minimum = Infinity;
1142
+ for (let index = 0; index < values.length; index += 1) {
1143
+ const value = values[index];
1144
+ if (Number.isFinite(value) && value < minimum) minimum = value;
1145
+ }
1146
+ return Number.isFinite(minimum) ? minimum : null;
1147
+ }
1148
+
1149
+ function nearestBoundaryDistance(firstDistance, firstSegmentId, secondDistance, secondSegmentId) {
1150
+ if (!Number.isFinite(firstDistance)) return Number.isFinite(secondDistance) ? secondDistance : null;
1151
+ if (!Number.isFinite(secondDistance)) return firstDistance;
1152
+ if (!Number.isInteger(firstSegmentId)) return secondDistance;
1153
+ if (!Number.isInteger(secondSegmentId)) return firstDistance;
1154
+ return firstDistance <= secondDistance ? firstDistance : secondDistance;
1155
+ }
1156
+
1157
+ function nearestBoundarySegmentId(firstDistance, firstSegmentId, secondDistance, secondSegmentId) {
1158
+ if (!Number.isFinite(firstDistance)) return Number.isInteger(secondSegmentId) ? secondSegmentId : null;
1159
+ if (!Number.isFinite(secondDistance)) return Number.isInteger(firstSegmentId) ? firstSegmentId : null;
1160
+ return firstDistance <= secondDistance
1161
+ ? (Number.isInteger(firstSegmentId) ? firstSegmentId : null)
1162
+ : (Number.isInteger(secondSegmentId) ? secondSegmentId : null);
1163
+ }
1164
+
1165
+ function finitePoint(point) {
1166
+ return Number.isFinite(point?.x) && Number.isFinite(point?.y);
1167
+ }
1168
+
1169
+ function finiteVector(vector) {
1170
+ return Number.isFinite(vector?.x) && Number.isFinite(vector?.y);
1171
+ }