@inventure71/paddockjs 3.0.0 → 4.1.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 (97) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/INSTALL_AND_UPDATE.md +21 -4
  3. package/README.md +103 -551
  4. package/docs/architecture.md +28 -18
  5. package/docs/component_inventory.md +5 -4
  6. package/docs/composable-layouts.md +79 -0
  7. package/docs/custom_model_controller.md +4 -4
  8. package/docs/data-helpers.md +50 -0
  9. package/docs/data_contract.md +13 -7
  10. package/docs/getting-started.md +105 -0
  11. package/docs/headless-environment.md +68 -0
  12. package/docs/index.md +17 -4
  13. package/docs/rules.md +6 -2
  14. package/docs/system_specs.md +60 -7
  15. package/docs/theming.md +64 -0
  16. package/docs/training.md +20 -14
  17. package/docs/troubleshooting.md +76 -0
  18. package/docs/upgrading-hosts-to-3.md +42 -0
  19. package/examples/python/README.md +6 -0
  20. package/examples/python/base_policy_server.py +19 -0
  21. package/package.json +21 -5
  22. package/src/api/PaddockSimulatorController.js +78 -22
  23. package/src/api/runtimeTheme.js +59 -0
  24. package/src/app/F1SimulatorApp.js +24 -3
  25. package/src/app/readouts/timingTowerRenderer.js +203 -42
  26. package/src/app/runtime/frameLoop.js +5 -1
  27. package/src/config/defaultOptions.js +8 -1
  28. package/src/config/themeOptions.js +5 -0
  29. package/src/data/index.d.ts +212 -0
  30. package/src/data/index.js +14 -0
  31. package/src/environment/episode.js +11 -1
  32. package/src/environment/metrics.js +59 -16
  33. package/src/environment/observationVector.js +50 -33
  34. package/src/environment/observations.js +89 -20
  35. package/src/environment/runtime.js +41 -0
  36. package/src/environment/sensors/carRays.js +48 -37
  37. package/src/environment/sensors/index.js +501 -97
  38. package/src/environment/sensors/indexedRayBands.js +134 -46
  39. package/src/environment/sensors/rayBandTrace.js +1171 -0
  40. package/src/environment/sensors/rayChannels.js +108 -0
  41. package/src/environment/sensors/rayConfig.js +71 -25
  42. package/src/environment/sensors/rayGeometry.js +23 -17
  43. package/src/environment/sensors/sensorTargets.js +67 -1
  44. package/src/environment/sensors/surfaceRays.js +130 -89
  45. package/src/environment/sensors/trackRays.js +115 -1
  46. package/src/index.d.ts +43 -0
  47. package/src/index.js +27 -2
  48. package/src/placeholder/index.d.ts +11 -0
  49. package/src/placeholder/index.js +61 -0
  50. package/src/placeholder.css +72 -0
  51. package/src/simulation/collisionGeometry.js +260 -96
  52. package/src/simulation/pit/pitFlow.js +21 -9
  53. package/src/simulation/pit/pitQueue.js +5 -3
  54. package/src/simulation/pit/pitRouteMovement.js +9 -4
  55. package/src/simulation/pit/pitRouting.js +77 -42
  56. package/src/simulation/pit/pitState.js +1 -0
  57. package/src/simulation/pit/pitTireService.js +7 -3
  58. package/src/simulation/race/raceFinish.js +25 -8
  59. package/src/simulation/race/raceOrder.js +189 -32
  60. package/src/simulation/race/raceProgress.js +129 -37
  61. package/src/simulation/race/raceSetup.js +2 -0
  62. package/src/simulation/race/raceStep.js +25 -13
  63. package/src/simulation/raceSimulation.js +20 -17
  64. package/src/simulation/rules/collisionSteward.js +40 -23
  65. package/src/simulation/rules/rulesReview.js +77 -56
  66. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  67. package/src/simulation/timing/drsTiming.js +143 -30
  68. package/src/simulation/timing/gapEstimation.js +25 -3
  69. package/src/simulation/timing/lapTelemetry.js +115 -36
  70. package/src/simulation/timing/raceTiming.js +5 -1
  71. package/src/simulation/timing/sectorPerformance.js +104 -26
  72. package/src/simulation/timing/timingHistory.js +229 -14
  73. package/src/simulation/timing/timingLines.js +65 -7
  74. package/src/simulation/track/pitLaneLayout.js +23 -0
  75. package/src/simulation/track/pitLaneState.js +218 -19
  76. package/src/simulation/track/spatialQueries.js +67 -11
  77. package/src/simulation/track/trackMath.js +91 -24
  78. package/src/simulation/track/trackModel.js +2 -0
  79. package/src/simulation/track/trackQueryIndex.js +1334 -100
  80. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  81. package/src/simulation/track/trackQueryScratch.js +7 -0
  82. package/src/simulation/track/trackQueryStats.js +25 -0
  83. package/src/simulation/track/trackSnapshotJson.js +139 -0
  84. package/src/simulation/track/trackStatePolicy.js +249 -1
  85. package/src/simulation/vehicle/contactResolution.js +96 -59
  86. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  87. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  88. package/src/simulation/vehicle/runoffResponse.js +54 -29
  89. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  90. package/src/simulation/vehicle/vehiclePhysics.js +42 -27
  91. package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
  92. package/src/simulation/vehicle/vehicleState.js +6 -0
  93. package/src/simulation/vehicle/wheelSurface.js +807 -92
  94. package/src/styles.css +497 -497
  95. package/src/ui/raceControlsTemplate.js +1 -0
  96. package/src/ui/telemetryDrawerTemplate.js +1 -0
  97. package/src/ui/telemetryTemplates.js +1 -0
@@ -1,6 +1,9 @@
1
1
  import { createSpatialGrid, insertIdIntoGridBounds, candidateIdsFromGrid } from './trackQueryGrid.js';
2
2
  import { recordFallback, recordStat } from './trackQueryStats.js';
3
3
 
4
+ const PIT_ROAD_GRID_NEIGHBOR_LIMIT = 1;
5
+ const PIT_ROAD_ENDPOINT_SEGMENT_COUNT = 4;
6
+
4
7
  export function createPitQueryIndex(track, cellSize) {
5
8
  const pitLane = track.pitLane;
6
9
  if (!pitLane?.enabled) return null;
@@ -21,7 +24,11 @@ export function createPitQueryIndex(track, cellSize) {
21
24
  ['working', pitLane.workingLane?.points],
22
25
  ['exit', pitLane.exit?.roadCenterline],
23
26
  ].forEach(([routeId, points]) => {
24
- routes[routeId] = { start: roadSegments.length };
27
+ routes[routeId] = {
28
+ start: roadSegments.length,
29
+ cumulativeDistances: createRouteCumulativeDistances(points),
30
+ ...createRouteEndpointSegmentWindows(points),
31
+ };
25
32
  indexPitRoute(roadGrid, roadSegments, routeId, points, cellSize);
26
33
  routes[routeId].end = roadSegments.length;
27
34
  });
@@ -50,7 +57,7 @@ export function createPitQueryIndex(track, cellSize) {
50
57
  };
51
58
  }
52
59
 
53
- export function queryPitBoxCandidates(track, position) {
60
+ export function queryPitBoxCandidates(track, position, scratch = null) {
54
61
  const index = track?.queryIndex;
55
62
  const pit = index?.pit;
56
63
  if (!pit?.boxGrid) {
@@ -65,18 +72,36 @@ export function queryPitBoxCandidates(track, position) {
65
72
  const ids = candidateIdsFromGrid(index, pit.boxGrid, position, 1);
66
73
  if (!ids.length) {
67
74
  recordStat(index, 'pitPaths', 'box-grid-miss');
75
+ if (scratch) {
76
+ const empty = scratch.pitBoxCandidates ?? [];
77
+ scratch.pitBoxCandidates = empty;
78
+ empty.length = 0;
79
+ return empty;
80
+ }
68
81
  return [];
69
82
  }
70
83
  recordStat(index, 'pitPaths', 'box-grid-hit');
71
- return ids.map((id) => pit.boxCandidates[id]).filter(Boolean);
84
+ const candidates = scratch ? (scratch.pitBoxCandidates ??= []) : [];
85
+ candidates.length = 0;
86
+ ids.forEach((id) => {
87
+ const candidate = pit.boxCandidates[id];
88
+ if (candidate) candidates.push(candidate);
89
+ });
90
+ return scratch ? candidates : [...candidates];
72
91
  }
73
92
 
74
- export function queryPitRoadSegmentCandidates(track, routeId, position) {
75
- const candidatesByRoute = queryPitRoadSegmentCandidatesByRoute(track, position);
93
+ export function queryPitRoadSegmentCandidates(track, routeId, position, scratch = null) {
94
+ const candidatesByRoute = queryPitRoadSegmentCandidatesByRoute(track, position, null, scratch);
76
95
  return candidatesByRoute?.[routeId] ?? candidatesByRoute;
77
96
  }
78
97
 
79
- export function queryPitRoadSegmentCandidatesByRoute(track, position) {
98
+ export function queryPitRoadSegmentCandidatesByRoute(
99
+ track,
100
+ position,
101
+ progressHintOrScratch = null,
102
+ scratchArg = null,
103
+ options = {},
104
+ ) {
80
105
  const index = track?.queryIndex;
81
106
  const pit = index?.pit;
82
107
  if (!pit?.roadGrid) {
@@ -87,22 +112,87 @@ export function queryPitRoadSegmentCandidatesByRoute(track, position) {
87
112
  recordFallback(index, 'pitFallbackReasons', 'invalid-position');
88
113
  return null;
89
114
  }
90
- index.stats.pitQueries += 1;
91
- const ids = candidateIdsFromGrid(index, pit.roadGrid, position, 1);
92
- if (!ids.length) {
93
- recordStat(index, 'pitPaths', 'road-grid-miss');
94
- return null;
95
- }
96
- const candidatesByRoute = { entry: [], main: [], working: [], exit: [] };
115
+ const progressHint = Number.isFinite(progressHintOrScratch)
116
+ ? progressHintOrScratch
117
+ : null;
118
+ const scratch = progressHint === null && scratchArg == null && progressHintOrScratch && typeof progressHintOrScratch === 'object'
119
+ ? progressHintOrScratch
120
+ : scratchArg;
121
+ if (options.countQuery !== false) index.stats.pitQueries += 1;
122
+ const ids = candidateIdsFromGrid(index, pit.roadGrid, position, PIT_ROAD_GRID_NEIGHBOR_LIMIT);
123
+ const candidatesByRoute = scratch
124
+ ? (scratch.pitRouteCandidates ??= { entry: [], main: [], working: [], exit: [] })
125
+ : { entry: [], main: [], working: [], exit: [] };
126
+ candidatesByRoute.entry.length = 0;
127
+ candidatesByRoute.main.length = 0;
128
+ candidatesByRoute.working.length = 0;
129
+ candidatesByRoute.exit.length = 0;
97
130
  ids.forEach((id) => {
98
131
  const segment = pit.roadSegments[id];
99
132
  if (!segment || !candidatesByRoute[segment.routeId]) return;
100
133
  candidatesByRoute[segment.routeId].push(segment.segmentIndex);
101
134
  });
102
- recordStat(index, 'pitPaths', 'road-grid-hit');
135
+ const recordPathStats = options.recordPathStats !== false;
136
+ const endpointAugmented = appendEndpointSegmentsForProgress(track, candidatesByRoute, progressHint);
137
+ const hasAnyCandidates = (
138
+ candidatesByRoute.entry.length +
139
+ candidatesByRoute.main.length +
140
+ candidatesByRoute.working.length +
141
+ candidatesByRoute.exit.length
142
+ ) > 0;
143
+ if (!hasAnyCandidates) {
144
+ if (recordPathStats) recordStat(index, 'pitPaths', 'road-grid-miss');
145
+ return null;
146
+ }
147
+ if (recordPathStats) recordStat(index, 'pitPaths', ids.length ? 'road-grid-hit' : 'road-grid-endpoint-hit');
148
+ if (recordPathStats && endpointAugmented) {
149
+ recordStat(index, 'pitPaths', 'road-endpoint-window-hit');
150
+ }
103
151
  return candidatesByRoute;
104
152
  }
105
153
 
154
+ function appendEndpointSegmentsForProgress(track, candidatesByRoute, progressHint) {
155
+ if (!Number.isFinite(progressHint)) return false;
156
+ const pit = track?.queryIndex?.pit;
157
+ const pitLane = track?.pitLane;
158
+ if (!pit?.routes || !pitLane?.enabled) return false;
159
+ let augmented = false;
160
+ if (progressHint >= pitLane.entry.trackDistance) {
161
+ augmented = appendRouteEndpointSegments(
162
+ candidatesByRoute.entry,
163
+ pit.routes.entry,
164
+ 'start',
165
+ ) || augmented;
166
+ }
167
+ if (progressHint <= pitLane.exit.trackDistance) {
168
+ augmented = appendRouteEndpointSegments(
169
+ candidatesByRoute.exit,
170
+ pit.routes.exit,
171
+ 'end',
172
+ ) || augmented;
173
+ }
174
+ return augmented;
175
+ }
176
+
177
+ function appendRouteEndpointSegments(target, route, edge) {
178
+ if (!Array.isArray(target) || !route) return false;
179
+ const beforeLength = target.length;
180
+ if (edge === 'start') {
181
+ const limit = Math.min(route.end, route.start + PIT_ROAD_ENDPOINT_SEGMENT_COUNT);
182
+ for (let id = route.start; id < limit; id += 1) {
183
+ const segmentIndex = id - route.start;
184
+ if (!target.includes(segmentIndex)) target.push(segmentIndex);
185
+ }
186
+ } else {
187
+ const limit = Math.max(route.start, route.end - PIT_ROAD_ENDPOINT_SEGMENT_COUNT);
188
+ for (let id = limit; id < route.end; id += 1) {
189
+ const segmentIndex = id - route.start;
190
+ if (!target.includes(segmentIndex)) target.push(segmentIndex);
191
+ }
192
+ }
193
+ return target.length > beforeLength;
194
+ }
195
+
106
196
  function indexPitRoute(grid, roadSegments, routeId, points, expansion) {
107
197
  if (!Array.isArray(points) || points.length < 2) return;
108
198
  for (let index = 0; index < points.length - 1; index += 1) {
@@ -120,6 +210,40 @@ function indexPitRoute(grid, roadSegments, routeId, points, expansion) {
120
210
  }
121
211
  }
122
212
 
213
+ function createRouteCumulativeDistances(points) {
214
+ if (!Array.isArray(points) || points.length === 0) return null;
215
+ const distances = new Array(points.length);
216
+ distances[0] = 0;
217
+ for (let index = 1; index < points.length; index += 1) {
218
+ const previous = points[index - 1];
219
+ const current = points[index];
220
+ distances[index] = distances[index - 1] + Math.hypot(current.x - previous.x, current.y - previous.y);
221
+ }
222
+ return distances;
223
+ }
224
+
225
+ function createRouteEndpointSegmentWindows(points) {
226
+ const segmentCount = Array.isArray(points) ? Math.max(0, points.length - 1) : 0;
227
+ const startSegmentWindow = [];
228
+ const endSegmentWindow = [];
229
+ const endpointSegmentWindow = [];
230
+ const startLimit = Math.min(segmentCount, PIT_ROAD_ENDPOINT_SEGMENT_COUNT);
231
+ const endStart = Math.max(0, segmentCount - PIT_ROAD_ENDPOINT_SEGMENT_COUNT);
232
+ for (let index = 0; index < startLimit; index += 1) {
233
+ startSegmentWindow.push(index);
234
+ endpointSegmentWindow.push(index);
235
+ }
236
+ for (let index = endStart; index < segmentCount; index += 1) {
237
+ endSegmentWindow.push(index);
238
+ if (!endpointSegmentWindow.includes(index)) endpointSegmentWindow.push(index);
239
+ }
240
+ return {
241
+ startSegmentWindow,
242
+ endSegmentWindow,
243
+ endpointSegmentWindow,
244
+ };
245
+ }
246
+
123
247
  function polygonBounds(polygon, expansion) {
124
248
  const bounds = polygon.reduce((current, point) => ({
125
249
  minX: Math.min(current.minX, point.x),
@@ -18,7 +18,14 @@ export function createQueryScratch(index) {
18
18
  rayCellEpoch: 1,
19
19
  raySegmentIds: [],
20
20
  rayVisitedCells: [],
21
+ rayTracePoint: { x: 0, y: 0 },
21
22
  rayTraceCache: new Map(),
23
+ hintDistanceCacheDistance: Number.NaN,
24
+ hintDistanceCacheWrappedDistance: Number.NaN,
25
+ hintDistanceCacheSegmentId: -1,
26
+ hintDistanceCachePointX: Number.NaN,
27
+ hintDistanceCachePointY: Number.NaN,
28
+ segmentNeighborhoodProjection: {},
22
29
  };
23
30
  }
24
31
 
@@ -2,12 +2,37 @@ export function createQueryStats() {
2
2
  return {
3
3
  nearestQueries: 0,
4
4
  nearestFallbacks: 0,
5
+ hintDistanceCacheHits: 0,
6
+ precomputedSegmentNeighborhoodHits: 0,
7
+ nearestEmptyCellSkippedRings: 0,
8
+ nearestIsolatedHintQueries: 0,
9
+ nearestLowDensityHintQueries: 0,
10
+ nearestLowDensityCellExactQueries: 0,
11
+ nearestLowDensityCellDirectQueries: 0,
12
+ nearestSparseGridExactQueries: 0,
13
+ nearestRing2NeighborhoodExactQueries: 0,
14
+ arcBucketRadius2PrecomputedQueries: 0,
5
15
  nearestPaths: {},
6
16
  nearestFallbackReasons: {
7
17
  'spatial-grid-no-candidates': 0,
8
18
  },
19
+ hintedArcQueries: 0,
20
+ hintedSegmentFastPathQueries: 0,
21
+ hintedSegmentWideRadiusQueries: 0,
22
+ hintedSegmentWideRadiusHits: 0,
23
+ candidateProjectionObjectAllocations: 0,
24
+ raySegmentObjectAllocations: 0,
25
+ rayTraceCacheEntryAllocations: 0,
26
+ hintedArcPaths: {},
27
+ segmentNeighborhoodQueries: 0,
28
+ segmentNeighborhoodBatchCalls: 0,
29
+ segmentNeighborhoodPaths: {},
30
+ runoffRadius1Queries: 0,
31
+ runoffRadius2Queries: 0,
9
32
  pitQueries: 0,
10
33
  pitFallbacks: 0,
34
+ pitConnectorEndpointWindowProjectionCalls: 0,
35
+ pitConnectorFullRouteProjectionScans: 0,
11
36
  pitPaths: {},
12
37
  pitFallbackReasons: {},
13
38
  };
@@ -0,0 +1,139 @@
1
+ const TRACK_JSON_CACHE = new WeakMap();
2
+ const MAX_JSON_SAMPLES = 320;
3
+ const TRACK_SAMPLE_SCHEMA = Object.freeze([
4
+ 'x',
5
+ 'y',
6
+ 'distance',
7
+ 'heading',
8
+ 'normalX',
9
+ 'normalY',
10
+ 'curvature',
11
+ ]);
12
+
13
+ const OMIT_JSON_KEYS = new Set([
14
+ 'bounds',
15
+ 'boxBounds',
16
+ 'connectorBounds',
17
+ 'pitCrew',
18
+ ]);
19
+
20
+ const HIGH_PRECISION_JSON_KEYS = new Set([
21
+ 'heading',
22
+ 'normalX',
23
+ 'normalY',
24
+ 'curvature',
25
+ 'startRatio',
26
+ 'endRatio',
27
+ ]);
28
+
29
+ export function attachTrackSnapshotJsonSerializer(track) {
30
+ if (!track || typeof track !== 'object') return track;
31
+ Object.defineProperty(track, 'toJSON', {
32
+ configurable: true,
33
+ enumerable: false,
34
+ value: trackSnapshotToJson,
35
+ writable: false,
36
+ });
37
+ return track;
38
+ }
39
+
40
+ export function serializeTrackSnapshotJson(track) {
41
+ if (!track || typeof track !== 'object') return track;
42
+ const cached = TRACK_JSON_CACHE.get(track);
43
+ if (cached) return cached;
44
+ const serialized = cloneTrackJsonValue(track);
45
+ TRACK_JSON_CACHE.set(track, serialized);
46
+ return serialized;
47
+ }
48
+
49
+ function trackSnapshotToJson() {
50
+ return serializeTrackSnapshotJson(this);
51
+ }
52
+
53
+ function cloneTrackJsonValue(value, key = null) {
54
+ if (typeof value === 'number') return roundTrackJsonNumber(key, value);
55
+ if (!value || typeof value !== 'object') return value;
56
+ if (Array.isArray(value)) {
57
+ if (key === 'samples') return cloneTrackSamples(value);
58
+ return value.map((entry) => cloneTrackJsonValue(entry));
59
+ }
60
+ if (key === 'pitLane') return clonePitLaneJson(value);
61
+
62
+ const clone = {};
63
+ Object.entries(value).forEach(([childKey, childValue]) => {
64
+ if (OMIT_JSON_KEYS.has(childKey)) return;
65
+ clone[childKey] = cloneTrackJsonValue(childValue, childKey);
66
+ });
67
+ if (key == null && Array.isArray(clone.samples)) clone.sampleSchema = TRACK_SAMPLE_SCHEMA;
68
+ return clone;
69
+ }
70
+
71
+ function cloneTrackSamples(samples) {
72
+ if (samples.length <= MAX_JSON_SAMPLES) {
73
+ return samples.map((entry) => serializeTrackSample(entry));
74
+ }
75
+ const lastIndex = samples.length - 1;
76
+ const stride = Math.max(1, Math.ceil(lastIndex / (MAX_JSON_SAMPLES - 1)));
77
+ const decimated = [];
78
+ for (let index = 0; index < lastIndex; index += stride) {
79
+ decimated.push(serializeTrackSample(samples[index]));
80
+ }
81
+ const lastSample = serializeTrackSample(samples[lastIndex]);
82
+ if (!decimated.length || decimated[decimated.length - 1]?.[2] !== lastSample?.[2]) {
83
+ decimated.push(lastSample);
84
+ }
85
+ return decimated;
86
+ }
87
+
88
+ function serializeTrackSample(sample) {
89
+ return [
90
+ roundTrackJsonNumber('x', sample?.x),
91
+ roundTrackJsonNumber('y', sample?.y),
92
+ roundTrackJsonNumber('distance', sample?.distance),
93
+ roundTrackJsonNumber('heading', sample?.heading),
94
+ roundTrackJsonNumber('normalX', sample?.normalX),
95
+ roundTrackJsonNumber('normalY', sample?.normalY),
96
+ roundTrackJsonNumber('curvature', sample?.curvature),
97
+ ];
98
+ }
99
+
100
+ function clonePitLaneJson(pitLane) {
101
+ const clone = {};
102
+ Object.entries(pitLane).forEach(([childKey, childValue]) => {
103
+ if (OMIT_JSON_KEYS.has(childKey)) return;
104
+ if (childKey === 'boxes') {
105
+ clone.boxes = childValue.map(clonePitBoxJson);
106
+ return;
107
+ }
108
+ if (childKey === 'serviceAreas') {
109
+ clone.serviceAreas = childValue.map(clonePitServiceAreaJson);
110
+ return;
111
+ }
112
+ clone[childKey] = cloneTrackJsonValue(childValue, childKey);
113
+ });
114
+ return clone;
115
+ }
116
+
117
+ function clonePitBoxJson(box) {
118
+ const clone = {};
119
+ Object.entries(box).forEach(([childKey, childValue]) => {
120
+ if (childKey === 'teamId' || childKey === 'teamName' || childKey === 'teamColor') return;
121
+ clone[childKey] = cloneTrackJsonValue(childValue, childKey);
122
+ });
123
+ return clone;
124
+ }
125
+
126
+ function clonePitServiceAreaJson(area) {
127
+ const clone = {};
128
+ Object.entries(area).forEach(([childKey, childValue]) => {
129
+ if (childKey === 'teamId' || childKey === 'teamName' || childKey === 'teamColor') return;
130
+ clone[childKey] = cloneTrackJsonValue(childValue, childKey);
131
+ });
132
+ return clone;
133
+ }
134
+
135
+ function roundTrackJsonNumber(key, value) {
136
+ if (!Number.isFinite(value)) return value;
137
+ const precision = HIGH_PRECISION_JSON_KEYS.has(key) ? 1_000_000 : 1_000;
138
+ return Math.round(value * precision) / precision;
139
+ }
@@ -1,4 +1,18 @@
1
- import { nearestTrackState } from './trackModel.js';
1
+ import { metersToSimUnits } from '../units.js';
2
+ import { pointInsideBounds } from './trackMath.js';
3
+ import { nearestTrackState, resolveTrackStatePitOverride, writeTrackState } from './spatialQueries.js';
4
+ import { nearestPitLaneState } from './pitLaneState.js';
5
+ import {
6
+ queryHintedTrackProjection,
7
+ querySegmentNeighborhoodProjection,
8
+ querySegmentNeighborhoodProjections,
9
+ } from './trackQueryIndex.js';
10
+
11
+ const LOCAL_SEGMENT_RADIUS = 3;
12
+ const RUNOFF_FAST_PATH_LOCAL_SEGMENT_RADIUS = 2;
13
+ const RUNOFF_FAST_PATH_SMALL_MOTION_RADIUS = 1;
14
+ const RUNOFF_FAST_PATH_SMALL_MOTION_LIMIT = metersToSimUnits(1.1);
15
+ const RUNOFF_FAST_PATH_TELEPORT_LIMIT = metersToSimUnits(80);
2
16
 
3
17
  export function pitOverrideAllowedForCar(car) {
4
18
  if (!car?.environmentControlled) return true;
@@ -21,3 +35,237 @@ export function nearestTrackStateForCar(
21
35
  allowPitOverride: resolvedAllowPitOverride,
22
36
  });
23
37
  }
38
+
39
+ export function queryHintedTrackStateForCar(
40
+ track,
41
+ car,
42
+ position = car,
43
+ progressHint = car?.progress ?? null,
44
+ options = {},
45
+ ) {
46
+ const resolvedAllowPitOverride = options.allowPitOverride ?? pitOverrideAllowedForCar(car);
47
+ const projection = queryHintedTrackProjection(track, position, progressHint, {
48
+ radius: options.radius ?? 2,
49
+ });
50
+ if (!projection) {
51
+ return nearestTrackState(track, position, progressHint, {
52
+ ...options,
53
+ allowPitOverride: resolvedAllowPitOverride,
54
+ });
55
+ }
56
+ const trackState = writeTrackState(options.target ?? {}, track, position, projection);
57
+ if (!resolvedAllowPitOverride) return trackState;
58
+ return resolveTrackStatePitOverride(track, trackState, nearestPitLaneState(track, position, progressHint));
59
+ }
60
+
61
+ export function queryLocalSegmentTrackStateForCar(
62
+ track,
63
+ car,
64
+ position = car,
65
+ baseSegmentId = car?.trackState?.segmentId ?? null,
66
+ progressHint = car?.progress ?? null,
67
+ options = {},
68
+ ) {
69
+ const resolvedAllowPitOverride = options.allowPitOverride ?? pitOverrideAllowedForCar(car);
70
+ const indexScratch = track?.queryIndex?.queryScratch;
71
+ const projectionTarget = options.targetProjection ?? (
72
+ indexScratch
73
+ ? (indexScratch.segmentNeighborhoodProjection ??= {})
74
+ : {}
75
+ );
76
+ const projection = querySegmentNeighborhoodProjection(track, position, baseSegmentId, {
77
+ radius: options.radius ?? LOCAL_SEGMENT_RADIUS,
78
+ preferredDistance: progressHint,
79
+ target: projectionTarget,
80
+ });
81
+ if (!projection) {
82
+ return queryHintedTrackStateForCar(track, car, position, progressHint, options);
83
+ }
84
+ const trackState = writeTrackState(options.target ?? {}, track, position, projection);
85
+ return finalizeTrackState(track, position, progressHint, trackState, resolvedAllowPitOverride, options);
86
+ }
87
+
88
+ export function queryLocalSegmentTrackStatesForCar(
89
+ track,
90
+ car,
91
+ positions,
92
+ baseSegmentId = car?.trackState?.segmentId ?? null,
93
+ progressHint = car?.progress ?? null,
94
+ options = {},
95
+ ) {
96
+ const resolvedAllowPitOverride = options.allowPitOverride ?? pitOverrideAllowedForCar(car);
97
+ const targets = Array.isArray(options.target) ? options.target : new Array(positions.length);
98
+ const projections = queryLocalSegmentTrackProjectionsForCar(
99
+ track,
100
+ car,
101
+ positions,
102
+ baseSegmentId,
103
+ progressHint,
104
+ options,
105
+ );
106
+ if (!projections) {
107
+ for (let index = 0; index < positions.length; index += 1) {
108
+ targets[index] = queryHintedTrackStateForCar(track, car, positions[index], progressHint, options);
109
+ }
110
+ return targets;
111
+ }
112
+ return writeLocalSegmentTrackStatesFromProjections(
113
+ track,
114
+ positions,
115
+ progressHint,
116
+ projections,
117
+ {
118
+ ...options,
119
+ allowPitOverride: resolvedAllowPitOverride,
120
+ target: targets,
121
+ },
122
+ );
123
+ }
124
+
125
+ export function queryLocalSegmentTrackProjectionsForCar(
126
+ track,
127
+ car,
128
+ positions,
129
+ baseSegmentId = car?.trackState?.segmentId ?? null,
130
+ progressHint = car?.progress ?? null,
131
+ options = {},
132
+ ) {
133
+ if (!Number.isInteger(baseSegmentId)) return null;
134
+ return querySegmentNeighborhoodProjections(track, positions, baseSegmentId, {
135
+ radius: options.radius ?? LOCAL_SEGMENT_RADIUS,
136
+ preferredDistance: progressHint,
137
+ target: options.projections,
138
+ });
139
+ }
140
+
141
+ export function writeLocalSegmentTrackStatesFromProjections(
142
+ track,
143
+ positions,
144
+ progressHint,
145
+ projections,
146
+ options = {},
147
+ ) {
148
+ if (!Array.isArray(projections)) return null;
149
+ const allowPitOverride = options.allowPitOverride !== false;
150
+ const targets = Array.isArray(options.target) ? options.target : new Array(positions.length);
151
+ for (let index = 0; index < positions.length; index += 1) {
152
+ const projection = projections[index];
153
+ if (!projection) {
154
+ targets[index] = null;
155
+ continue;
156
+ }
157
+ const trackState = writeTrackState(targets[index] ?? {}, track, positions[index], projection);
158
+ targets[index] = finalizeTrackState(track, positions[index], progressHint, trackState, allowPitOverride, options);
159
+ }
160
+ return targets;
161
+ }
162
+
163
+ export function queryRunoffTrackStateForCar(track, car, position = car) {
164
+ const allowPitOverride = pitOverrideAllowedForCar(car);
165
+ const previousState = car?.trackState;
166
+ const progressHint = car?.progress ?? previousState?.distance ?? null;
167
+ const scratch = car?.runoffTrackStateScratch ?? {
168
+ mainTrackState: {},
169
+ result: {
170
+ state: null,
171
+ mainTrackState: null,
172
+ usedFastPathProjection: false,
173
+ },
174
+ };
175
+ if (car && car.runoffTrackStateScratch == null) car.runoffTrackStateScratch = scratch;
176
+ const canUseHintedMainTrack = Number.isFinite(previousState?.distance) &&
177
+ !previousState?.inPitLane &&
178
+ !runoffMotionLooksLikeTeleport(car) &&
179
+ !runoffProgressJumpLooksLikeTeleport(track, previousState, progressHint) &&
180
+ (!allowPitOverride || !pitOverrideMayApply(track, position, previousState));
181
+
182
+ if (canUseHintedMainTrack) {
183
+ const localRadius = runoffFastPathLocalSegmentRadius(car);
184
+ const mainTrackState = queryLocalSegmentTrackStateForCar(track, car, position, previousState.segmentId, previousState.distance, {
185
+ allowPitOverride: false,
186
+ radius: localRadius,
187
+ target: scratch.mainTrackState,
188
+ });
189
+ recordRunoffRadiusUsage(track, localRadius);
190
+ scratch.result.state = mainTrackState;
191
+ scratch.result.mainTrackState = mainTrackState;
192
+ scratch.result.usedFastPathProjection = true;
193
+ return scratch.result;
194
+ }
195
+
196
+ const state = nearestTrackState(track, position, progressHint, { allowPitOverride });
197
+ const mainTrackState = state.inPitLane
198
+ ? nearestTrackState(track, position, progressHint, { allowPitOverride: false })
199
+ : state;
200
+ scratch.result.state = state;
201
+ scratch.result.mainTrackState = mainTrackState;
202
+ scratch.result.usedFastPathProjection = false;
203
+ return scratch.result;
204
+ }
205
+
206
+ function pitOverrideMayApply(track, position, previousState) {
207
+ const pitLane = track?.pitLane;
208
+ if (!pitLane || !previousState) return false;
209
+ const signedOffset = previousState.signedOffset ?? 0;
210
+ const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
211
+ if (Math.abs(signedOffset) <= mainRoadEdge) return false;
212
+ if (signedOffset * pitLane.side <= 0) return false;
213
+ const connectorBounds = pitLane.connectorBounds;
214
+ return Boolean(
215
+ connectorBounds && (
216
+ (connectorBounds.entry && pointInsideBounds(position, connectorBounds.entry)) ||
217
+ (connectorBounds.exit && pointInsideBounds(position, connectorBounds.exit))
218
+ ),
219
+ );
220
+ }
221
+
222
+ function runoffMotionLooksLikeTeleport(car) {
223
+ return runoffMotionDistance(car) > RUNOFF_FAST_PATH_TELEPORT_LIMIT;
224
+ }
225
+
226
+ function runoffProgressJumpLooksLikeTeleport(track, previousState, progressHint) {
227
+ if (!Number.isFinite(previousState?.distance) || !Number.isFinite(progressHint)) return true;
228
+ const trackLength = track?.length;
229
+ if (!Number.isFinite(trackLength) || trackLength <= 0) {
230
+ return Math.abs(progressHint - previousState.distance) > RUNOFF_FAST_PATH_TELEPORT_LIMIT;
231
+ }
232
+ const delta = Math.abs(progressHint - previousState.distance);
233
+ return Math.min(delta, trackLength - delta) > RUNOFF_FAST_PATH_TELEPORT_LIMIT;
234
+ }
235
+
236
+ function runoffFastPathLocalSegmentRadius(car) {
237
+ return runoffMotionDistance(car) <= RUNOFF_FAST_PATH_SMALL_MOTION_LIMIT
238
+ ? RUNOFF_FAST_PATH_SMALL_MOTION_RADIUS
239
+ : RUNOFF_FAST_PATH_LOCAL_SEGMENT_RADIUS;
240
+ }
241
+
242
+ function recordRunoffRadiusUsage(track, radius) {
243
+ const stats = track?.queryIndex?.stats;
244
+ if (!stats) return;
245
+ if (radius <= RUNOFF_FAST_PATH_SMALL_MOTION_RADIUS) {
246
+ stats.runoffRadius1Queries = (stats.runoffRadius1Queries ?? 0) + 1;
247
+ return;
248
+ }
249
+ stats.runoffRadius2Queries = (stats.runoffRadius2Queries ?? 0) + 1;
250
+ }
251
+
252
+ function finalizeTrackState(track, position, progressHint, trackState, allowPitOverride, options) {
253
+ if (!allowPitOverride) return trackState;
254
+ if (options.skipPitOverrideInsideMainRoad) {
255
+ const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
256
+ if (trackState.crossTrackError <= mainRoadEdge) return trackState;
257
+ }
258
+ return resolveTrackStatePitOverride(track, trackState, nearestPitLaneState(track, position, progressHint));
259
+ }
260
+
261
+ function runoffMotionDistance(car) {
262
+ if (
263
+ !Number.isFinite(car?.previousX) ||
264
+ !Number.isFinite(car?.previousY) ||
265
+ !Number.isFinite(car?.x) ||
266
+ !Number.isFinite(car?.y)
267
+ ) return 0;
268
+ const dx = car.x - car.previousX;
269
+ const dy = car.y - car.previousY;
270
+ return Math.hypot(dx, dy);
271
+ }