@inventure71/paddockjs 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/INSTALL_AND_UPDATE.md +1 -1
- package/README.md +30 -5
- package/docs/architecture.md +15 -9
- package/docs/concepts.md +6 -4
- package/docs/data_contract.md +28 -15
- package/docs/data_model.md +1 -1
- package/docs/index.md +9 -0
- package/docs/rules.md +37 -7
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +56 -7
- package/docs/training.md +16 -10
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +2 -33
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +1 -42
- package/src/config/restartOptions.js +34 -0
- package/src/environment/controllerLoop.js +17 -2
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +16 -2
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +62 -14
- package/src/environment/scenarios.js +22 -3
- package/src/environment/sensors/index.js +3 -3
- package/src/environment/sensors/opponentRadar.js +13 -3
- package/src/environment/sensors/sensorTargets.js +8 -2
- package/src/environment/sensors/surfaceRays.js +90 -26
- package/src/environment/sensors/trackRays.js +185 -22
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -32
- package/src/simulation/pit/pitFlow.js +5 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitQueue.js +5 -0
- package/src/simulation/pit/pitRouteMovement.js +7 -0
- package/src/simulation/pit/pitService.js +2 -0
- package/src/simulation/pit/pitTireService.js +2 -0
- package/src/simulation/race/raceLifecycle.js +2 -6
- package/src/simulation/race/raceProgress.js +6 -1
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/redFlag.js +3 -3
- package/src/simulation/race/safetyCar.js +10 -4
- package/src/simulation/race/stalledDnf.js +8 -17
- package/src/simulation/raceSimulation.js +1 -0
- package/src/simulation/rules/rulesReview.js +3 -0
- package/src/simulation/snapshots/raceSnapshots.js +1 -0
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +16 -302
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -0
- package/src/simulation/vehicle/contactResolution.js +67 -2
- package/src/simulation/vehicle/runoffResponse.js +11 -12
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +2 -1
- package/src/simulation/vehicle/vehicleSnapshots.js +11 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createCarRayMiss, estimateCarHit } from './carRays.js';
|
|
2
2
|
import { normalizeRayOptions } from './rayConfig.js';
|
|
3
3
|
import { degreesToRadians, getCarRayOrigin } from './rayGeometry.js';
|
|
4
|
-
import { rayDetectableTargetsForSnapshot } from './sensorTargets.js';
|
|
4
|
+
import { isSelfCarTarget, rayDetectableTargetsForSnapshot } from './sensorTargets.js';
|
|
5
5
|
import { createTrackMiss, createTrackRayContext, estimateTrackHit } from './trackRays.js';
|
|
6
6
|
import { createSurfaceMiss, estimateSurfaceHits, requestedSurfaceChannels } from './surfaceRays.js';
|
|
7
7
|
import { canUseBatchTrainingRayApproximation } from './rayGuards.js';
|
|
@@ -28,9 +28,9 @@ export function buildRaySensors(car, snapshot, rayOptions = {}, batchContext = n
|
|
|
28
28
|
requestedSurfaceChannels(normalized.channels).length > 0;
|
|
29
29
|
const trackContext = !usesTrackContext
|
|
30
30
|
? null
|
|
31
|
-
: createTrackRayContext(car, snapshot, origin);
|
|
31
|
+
: createTrackRayContext(car, snapshot, origin, normalized.precision);
|
|
32
32
|
const carTargets = normalized.channels.includes('car')
|
|
33
|
-
? (batchContext?.rayTargets ?? rayDetectableTargetsForSnapshot(snapshot)).filter((target) => target
|
|
33
|
+
? (batchContext?.rayTargets ?? rayDetectableTargetsForSnapshot(snapshot)).filter((target) => !isSelfCarTarget(car, target))
|
|
34
34
|
: [];
|
|
35
35
|
|
|
36
36
|
if (canUseFastBatchTrainingRays(car, snapshot, trackContext)) {
|
|
@@ -3,12 +3,12 @@ import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
|
|
|
3
3
|
|
|
4
4
|
export function enrichOpponentRadar(car, nearbyCars, snapshot) {
|
|
5
5
|
const targetsById = new Map([
|
|
6
|
-
...(snapshot.cars ?? []).map((entry) => [entry.id, entry]),
|
|
7
|
-
...(snapshot.replayGhosts ?? []).map((entry) => [entry.id, entry]),
|
|
6
|
+
...(snapshot.cars ?? []).map((entry) => [targetKey('car', entry.id), entry]),
|
|
7
|
+
...(snapshot.replayGhosts ?? []).map((entry) => [targetKey('replayGhost', entry.id), entry]),
|
|
8
8
|
]);
|
|
9
9
|
|
|
10
10
|
return nearbyCars.map((nearby) => {
|
|
11
|
-
const target = targetsById.get(nearby.id);
|
|
11
|
+
const target = targetsById.get(targetKey(nearby.entityType ?? 'car', nearby.id));
|
|
12
12
|
const derived = target ? deriveRadarFields(car, target, nearby) : fallbackRadarFields(nearby);
|
|
13
13
|
return {
|
|
14
14
|
...nearby,
|
|
@@ -17,6 +17,10 @@ export function enrichOpponentRadar(car, nearbyCars, snapshot) {
|
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function targetKey(entityType, id) {
|
|
21
|
+
return `${entityType}:${id}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
function deriveRadarFields(car, target, nearby) {
|
|
21
25
|
const dx = (target.x ?? 0) - (car.x ?? 0);
|
|
22
26
|
const dy = (target.y ?? 0) - (car.y ?? 0);
|
|
@@ -56,6 +60,12 @@ function fallbackRadarFields(nearby) {
|
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function velocity(car) {
|
|
63
|
+
if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
|
|
64
|
+
return {
|
|
65
|
+
x: car.velocityX,
|
|
66
|
+
y: car.velocityY,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
59
69
|
const speed = Number.isFinite(car.speed) ? car.speed : kphToSimSpeed(car.speedKph ?? 0);
|
|
60
70
|
const heading = car.heading ?? car.headingRadians ?? 0;
|
|
61
71
|
return {
|
|
@@ -7,11 +7,11 @@ export const SENSOR_TARGET_CAR = 'car';
|
|
|
7
7
|
export const SENSOR_TARGET_REPLAY_GHOST = 'replayGhost';
|
|
8
8
|
|
|
9
9
|
export function rayDetectableTargets(self, snapshot) {
|
|
10
|
-
return rayDetectableTargetsForSnapshot(snapshot).filter((target) => target
|
|
10
|
+
return rayDetectableTargetsForSnapshot(snapshot).filter((target) => !isSelfCarTarget(self, target));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function nearbyDetectableTargets(self, snapshot) {
|
|
14
|
-
return nearbyDetectableTargetsForSnapshot(snapshot).filter((target) => target
|
|
14
|
+
return nearbyDetectableTargetsForSnapshot(snapshot).filter((target) => !isSelfCarTarget(self, target));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function rayDetectableTargetsForSnapshot(snapshot) {
|
|
@@ -37,6 +37,8 @@ function detectableCars(snapshot, predicate) {
|
|
|
37
37
|
x: car.x,
|
|
38
38
|
y: car.y,
|
|
39
39
|
heading: car.heading,
|
|
40
|
+
velocityX: car.velocityX,
|
|
41
|
+
velocityY: car.velocityY,
|
|
40
42
|
speedKph: car.speedKph ?? 0,
|
|
41
43
|
lap: car.lap,
|
|
42
44
|
order,
|
|
@@ -58,3 +60,7 @@ function detectableReplayGhosts(snapshot, sensorFlag) {
|
|
|
58
60
|
order: carCount + index,
|
|
59
61
|
}));
|
|
60
62
|
}
|
|
63
|
+
|
|
64
|
+
export function isSelfCarTarget(self, target) {
|
|
65
|
+
return target.entityType === SENSOR_TARGET_CAR && target.id === self.id;
|
|
66
|
+
}
|
|
@@ -43,36 +43,47 @@ export function estimateSurfaceHits(
|
|
|
43
43
|
return misses;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const originState = context?.
|
|
47
|
-
|
|
48
|
-
car,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
const originState = context?.precision === precision
|
|
47
|
+
? context.originState
|
|
48
|
+
: nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
|
|
49
|
+
const originHits = surfaceHitsAtOrigin(requested, originState);
|
|
50
|
+
if (!usesSampledDriverContract(car, precision)) {
|
|
51
|
+
const indexedHits = estimateIndexedSurfaceHits({
|
|
52
|
+
car,
|
|
53
|
+
track: snapshot.track,
|
|
54
|
+
ray,
|
|
55
|
+
origin,
|
|
56
|
+
vector,
|
|
57
|
+
requested,
|
|
58
|
+
originState,
|
|
59
|
+
precision,
|
|
60
|
+
sharedRayQuery,
|
|
61
|
+
});
|
|
62
|
+
if (indexedHits) return { ...misses, ...indexedHits, ...originHits };
|
|
63
|
+
const analyticHits = estimateAnalyticSurfaceHits({
|
|
64
|
+
car,
|
|
65
|
+
track: snapshot.track,
|
|
66
|
+
ray,
|
|
67
|
+
vector,
|
|
68
|
+
requested,
|
|
69
|
+
originState,
|
|
70
|
+
});
|
|
71
|
+
if (analyticHits) return { ...misses, ...analyticHits, ...originHits };
|
|
72
|
+
}
|
|
67
73
|
|
|
68
74
|
const pending = new Set(requested);
|
|
75
|
+
Object.keys(originHits).forEach((channel) => {
|
|
76
|
+
misses[channel] = originHits[channel];
|
|
77
|
+
pending.delete(channel);
|
|
78
|
+
});
|
|
79
|
+
if (pending.size === 0) return misses;
|
|
69
80
|
const maxDistance = metersToSimUnits(ray.lengthMeters);
|
|
70
81
|
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
71
82
|
let previousDistance = 0;
|
|
72
83
|
let previousState = null;
|
|
73
84
|
|
|
74
85
|
for (let distance = 0; distance <= maxDistance; distance += step) {
|
|
75
|
-
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress);
|
|
86
|
+
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress, precision);
|
|
76
87
|
for (const channel of [...pending]) {
|
|
77
88
|
if (!matchesSurfaceChannel(channel, state)) continue;
|
|
78
89
|
const hitDistance = previousState
|
|
@@ -93,6 +104,19 @@ export function estimateSurfaceHits(
|
|
|
93
104
|
return misses;
|
|
94
105
|
}
|
|
95
106
|
|
|
107
|
+
function surfaceHitsAtOrigin(requested, originState) {
|
|
108
|
+
const hits = {};
|
|
109
|
+
requested.forEach((channel) => {
|
|
110
|
+
if (!matchesSurfaceChannel(channel, originState)) return;
|
|
111
|
+
hits[channel] = {
|
|
112
|
+
hit: true,
|
|
113
|
+
distanceMeters: 0,
|
|
114
|
+
surface: originState.surface ?? null,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
return hits;
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState }) {
|
|
97
121
|
if (!originState || car.inPitLane || originState.inPitLane) return null;
|
|
98
122
|
if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
|
|
@@ -138,7 +162,7 @@ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, origi
|
|
|
138
162
|
return hits;
|
|
139
163
|
}
|
|
140
164
|
|
|
141
|
-
function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, sharedRayQuery }) {
|
|
165
|
+
function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery }) {
|
|
142
166
|
if (!originState || car.inPitLane || originState.inPitLane) return null;
|
|
143
167
|
if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
|
|
144
168
|
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
|
|
@@ -168,9 +192,23 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
|
|
|
168
192
|
? minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
|
|
169
193
|
: boundaries.kerbOuterDistance;
|
|
170
194
|
if (boundaryDistance == null) return null;
|
|
195
|
+
const hitDistance = precision === 'debug' || usesMainTrackOnlyRays(car)
|
|
196
|
+
? boundaryDistance
|
|
197
|
+
: sampledIndexedSurfaceDistance({
|
|
198
|
+
track,
|
|
199
|
+
car,
|
|
200
|
+
origin,
|
|
201
|
+
vector,
|
|
202
|
+
progressHint: car.progress,
|
|
203
|
+
distance: boundaryDistance,
|
|
204
|
+
maxDistance: metersToSimUnits(ray.lengthMeters),
|
|
205
|
+
channel,
|
|
206
|
+
precision,
|
|
207
|
+
});
|
|
208
|
+
if (hitDistance == null) return null;
|
|
171
209
|
hits[channel] = {
|
|
172
210
|
hit: true,
|
|
173
|
-
distanceMeters: simUnitsToMeters(
|
|
211
|
+
distanceMeters: simUnitsToMeters(hitDistance),
|
|
174
212
|
surface: channel === 'kerb'
|
|
175
213
|
? 'kerb'
|
|
176
214
|
: surfaceBeyondKerb(track, originState.signedOffset ?? 0, lateral, metersToSimUnits(ray.lengthMeters)),
|
|
@@ -180,6 +218,28 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
|
|
|
180
218
|
return hits;
|
|
181
219
|
}
|
|
182
220
|
|
|
221
|
+
function sampledIndexedSurfaceDistance({
|
|
222
|
+
track,
|
|
223
|
+
car,
|
|
224
|
+
origin,
|
|
225
|
+
vector,
|
|
226
|
+
progressHint,
|
|
227
|
+
distance,
|
|
228
|
+
maxDistance,
|
|
229
|
+
channel,
|
|
230
|
+
precision,
|
|
231
|
+
}) {
|
|
232
|
+
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
233
|
+
const firstIndex = Math.max(0, Math.floor(Math.max(0, distance - step * 1.5) / step));
|
|
234
|
+
const lastIndex = Math.ceil(Math.min(maxDistance, distance + step * 8) / step);
|
|
235
|
+
for (let index = firstIndex; index <= lastIndex; index += 1) {
|
|
236
|
+
const sampleDistance = Math.min(maxDistance, index * step);
|
|
237
|
+
const state = nearestRayTrackState(track, car, pointOnRay(origin, vector, sampleDistance), progressHint, precision);
|
|
238
|
+
if (matchesSurfaceChannel(channel, state)) return sampleDistance;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
183
243
|
function usesMainTrackOnlyRays(car) {
|
|
184
244
|
return car?.interaction?.profile === 'batch-training';
|
|
185
245
|
}
|
|
@@ -281,15 +341,19 @@ function matchesSurfaceChannel(channel, state) {
|
|
|
281
341
|
return false;
|
|
282
342
|
}
|
|
283
343
|
|
|
284
|
-
function nearestRayTrackState(track, car, point, progressHint) {
|
|
344
|
+
function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
|
|
285
345
|
const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
|
|
286
346
|
return nearestTrackState(track, point, progressHint, {
|
|
287
347
|
allowPitOverride: pitOverrideAllowedForCar(car),
|
|
288
|
-
indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
|
|
348
|
+
indexMode: usesSampledDriverContract(car, precision) ? 'legacy' : useBatchTrainingIndexMode ? 'sample' : undefined,
|
|
289
349
|
hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
|
|
290
350
|
});
|
|
291
351
|
}
|
|
292
352
|
|
|
353
|
+
function usesSampledDriverContract(car, precision) {
|
|
354
|
+
return precision === 'driver' && !usesMainTrackOnlyRays(car);
|
|
355
|
+
}
|
|
356
|
+
|
|
293
357
|
function minFinite(...values) {
|
|
294
358
|
const minimum = Math.min(...values.filter((value) => Number.isFinite(value)));
|
|
295
359
|
return Number.isFinite(minimum) ? minimum : null;
|
|
@@ -12,13 +12,14 @@ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
|
|
|
12
12
|
import { degreesToRadians, getCarRayOrigin, getCarRayVector, pointOnRay } from './rayGeometry.js';
|
|
13
13
|
import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
|
|
14
14
|
|
|
15
|
-
export function createTrackRayContext(car, snapshot, origin) {
|
|
15
|
+
export function createTrackRayContext(car, snapshot, origin, precision = 'debug') {
|
|
16
16
|
if (!Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
|
|
17
|
-
return { origin, originState: null };
|
|
17
|
+
return { origin, originState: null, precision };
|
|
18
18
|
}
|
|
19
19
|
return {
|
|
20
20
|
origin,
|
|
21
|
-
originState: nearestRayTrackState(snapshot.track, car, origin, car.progress),
|
|
21
|
+
originState: nearestRayTrackState(snapshot.track, car, origin, car.progress, precision),
|
|
22
|
+
precision,
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -38,19 +39,36 @@ export function estimateTrackHit(
|
|
|
38
39
|
const ray = getCarRayVector(car, angleDegrees);
|
|
39
40
|
const maxDistance = metersToSimUnits(lengthMeters);
|
|
40
41
|
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
41
|
-
const originState = context?.
|
|
42
|
+
const originState = context?.precision === precision
|
|
43
|
+
? context.originState
|
|
44
|
+
: nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
|
|
42
45
|
const includePitLane = Boolean(car.inPitLane || car.pitLanePart || originState.inPitLane);
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
const directionalAnalyticHit = canUseDirectionalAnalyticTrackHit({ car, track: snapshot.track, originState, ray, includePitLane })
|
|
47
|
+
? estimateAnalyticMainTrackHit({
|
|
48
|
+
car,
|
|
49
|
+
track: snapshot.track,
|
|
50
|
+
origin,
|
|
51
|
+
originState,
|
|
52
|
+
ray,
|
|
53
|
+
lengthMeters,
|
|
54
|
+
includePitLane,
|
|
55
|
+
})
|
|
56
|
+
: null;
|
|
57
|
+
if (directionalAnalyticHit) return directionalAnalyticHit;
|
|
58
|
+
if (!usesSampledDriverContract(car, precision)) {
|
|
59
|
+
const indexedHit = estimateIndexedTrackHit({
|
|
60
|
+
car,
|
|
61
|
+
track: snapshot.track,
|
|
62
|
+
origin,
|
|
63
|
+
originState,
|
|
64
|
+
ray,
|
|
65
|
+
lengthMeters,
|
|
66
|
+
includePitLane,
|
|
67
|
+
precision,
|
|
68
|
+
sharedRayQuery,
|
|
69
|
+
});
|
|
70
|
+
if (indexedHit) return indexedHit;
|
|
71
|
+
}
|
|
54
72
|
const analyticHit = estimateAnalyticMainTrackHit({
|
|
55
73
|
car,
|
|
56
74
|
track: snapshot.track,
|
|
@@ -65,7 +83,7 @@ export function estimateTrackHit(
|
|
|
65
83
|
let previousInside = null;
|
|
66
84
|
|
|
67
85
|
for (let distance = 0; distance <= maxDistance; distance += step) {
|
|
68
|
-
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, ray, distance), car.progress);
|
|
86
|
+
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, ray, distance), car.progress, precision);
|
|
69
87
|
const inside = isInsideTrackBorder(state, snapshot.track, includePitLane);
|
|
70
88
|
if (previousInside == null) {
|
|
71
89
|
previousInside = inside;
|
|
@@ -86,6 +104,7 @@ export function estimateTrackHit(
|
|
|
86
104
|
kind,
|
|
87
105
|
includePitLane,
|
|
88
106
|
refineStepsForPrecision(precision),
|
|
107
|
+
precision,
|
|
89
108
|
);
|
|
90
109
|
return {
|
|
91
110
|
hit: true,
|
|
@@ -138,7 +157,16 @@ function estimateAnalyticMainTrackHit({ car, track, origin, originState, ray, le
|
|
|
138
157
|
};
|
|
139
158
|
}
|
|
140
159
|
|
|
141
|
-
function
|
|
160
|
+
function canUseDirectionalAnalyticTrackHit({ car, track, originState, ray, includePitLane }) {
|
|
161
|
+
if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return false;
|
|
162
|
+
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
|
|
163
|
+
if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return false;
|
|
164
|
+
|
|
165
|
+
const lateral = ray.x * originState.normalX + ray.y * originState.normalY;
|
|
166
|
+
return Math.abs(lateral) >= 0.08;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane, precision, sharedRayQuery }) {
|
|
142
170
|
if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return null;
|
|
143
171
|
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
|
|
144
172
|
const trackHalfWidth = track.width / 2;
|
|
@@ -154,10 +182,25 @@ function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthM
|
|
|
154
182
|
);
|
|
155
183
|
if (!boundaries.available) return null;
|
|
156
184
|
if (boundaries.trackEdgeDistance == null) return null;
|
|
185
|
+
const kind = inside ? 'exit' : 'entry';
|
|
186
|
+
const hitDistance = validatedIndexedTransitionDistance({
|
|
187
|
+
track,
|
|
188
|
+
car,
|
|
189
|
+
origin,
|
|
190
|
+
ray,
|
|
191
|
+
progressHint: car.progress,
|
|
192
|
+
distance: boundaries.trackEdgeDistance,
|
|
193
|
+
maxDistance: metersToSimUnits(lengthMeters),
|
|
194
|
+
kind,
|
|
195
|
+
includePitLane,
|
|
196
|
+
precision,
|
|
197
|
+
originInside: inside,
|
|
198
|
+
});
|
|
199
|
+
if (hitDistance == null) return null;
|
|
157
200
|
return {
|
|
158
201
|
hit: true,
|
|
159
|
-
distanceMeters: simUnitsToMeters(
|
|
160
|
-
kind
|
|
202
|
+
distanceMeters: simUnitsToMeters(hitDistance),
|
|
203
|
+
kind,
|
|
161
204
|
};
|
|
162
205
|
}
|
|
163
206
|
|
|
@@ -223,12 +266,13 @@ function refineTrackTransitionDistance(
|
|
|
223
266
|
kind,
|
|
224
267
|
includePitLane = true,
|
|
225
268
|
refineSteps = TRACK_RAY_REFINE_STEPS,
|
|
269
|
+
precision = 'debug',
|
|
226
270
|
) {
|
|
227
271
|
let low = lowDistance;
|
|
228
272
|
let high = highDistance;
|
|
229
273
|
for (let index = 0; index < refineSteps; index += 1) {
|
|
230
274
|
const middle = (low + high) / 2;
|
|
231
|
-
const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint);
|
|
275
|
+
const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint, precision);
|
|
232
276
|
const inside = isInsideTrackBorder(state, track, includePitLane);
|
|
233
277
|
if (kind === 'entry') {
|
|
234
278
|
if (inside) high = middle;
|
|
@@ -242,15 +286,19 @@ function refineTrackTransitionDistance(
|
|
|
242
286
|
return high;
|
|
243
287
|
}
|
|
244
288
|
|
|
245
|
-
function nearestRayTrackState(track, car, point, progressHint) {
|
|
289
|
+
function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
|
|
246
290
|
const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
|
|
247
291
|
return nearestTrackState(track, point, progressHint, {
|
|
248
292
|
allowPitOverride: pitOverrideAllowedForCar(car),
|
|
249
|
-
indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
|
|
293
|
+
indexMode: usesSampledDriverContract(car, precision) ? 'legacy' : useBatchTrainingIndexMode ? 'sample' : undefined,
|
|
250
294
|
hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
|
|
251
295
|
});
|
|
252
296
|
}
|
|
253
297
|
|
|
298
|
+
function usesSampledDriverContract(car, precision) {
|
|
299
|
+
return precision === 'driver' && !usesMainTrackOnlyRays(car);
|
|
300
|
+
}
|
|
301
|
+
|
|
254
302
|
function refineStepsForPrecision(precision) {
|
|
255
303
|
return precision === 'debug' ? TRACK_RAY_REFINE_STEPS : DRIVER_RAY_REFINE_STEPS;
|
|
256
304
|
}
|
|
@@ -259,3 +307,118 @@ function isInsideTrackBorder(state, track, includePitLane = true) {
|
|
|
259
307
|
if (includePitLane && state.inPitLane) return true;
|
|
260
308
|
return state.crossTrackError <= track.width / 2;
|
|
261
309
|
}
|
|
310
|
+
|
|
311
|
+
function validatedIndexedTransitionDistance({
|
|
312
|
+
track,
|
|
313
|
+
car,
|
|
314
|
+
origin,
|
|
315
|
+
ray,
|
|
316
|
+
progressHint,
|
|
317
|
+
distance,
|
|
318
|
+
maxDistance,
|
|
319
|
+
kind,
|
|
320
|
+
includePitLane,
|
|
321
|
+
precision = 'driver',
|
|
322
|
+
originInside = kind === 'exit',
|
|
323
|
+
}) {
|
|
324
|
+
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
325
|
+
const refineSteps = refineStepsForPrecision(precision);
|
|
326
|
+
if (refineSteps <= 0 && usesMainTrackOnlyRays(car)) return distance;
|
|
327
|
+
if (refineSteps <= 0) {
|
|
328
|
+
return sampledIndexedTransitionDistance({
|
|
329
|
+
track,
|
|
330
|
+
car,
|
|
331
|
+
origin,
|
|
332
|
+
ray,
|
|
333
|
+
progressHint,
|
|
334
|
+
distance,
|
|
335
|
+
maxDistance,
|
|
336
|
+
kind,
|
|
337
|
+
includePitLane,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const windows = [step * 1.5, step * 8];
|
|
342
|
+
|
|
343
|
+
for (const searchWindow of windows) {
|
|
344
|
+
const low = Math.max(0, distance - searchWindow);
|
|
345
|
+
const high = Math.min(maxDistance, distance + searchWindow);
|
|
346
|
+
if (high <= low) continue;
|
|
347
|
+
const previousInside = validatedInsideTrackBorder(track, car, origin, ray, low, progressHint, includePitLane);
|
|
348
|
+
const inside = validatedInsideTrackBorder(track, car, origin, ray, high, progressHint, includePitLane);
|
|
349
|
+
const matched = kind === 'entry'
|
|
350
|
+
? !previousInside && inside
|
|
351
|
+
: previousInside && !inside;
|
|
352
|
+
if (matched) {
|
|
353
|
+
if (refineSteps <= 0) return distance;
|
|
354
|
+
return refineValidatedTransitionDistance(
|
|
355
|
+
track,
|
|
356
|
+
car,
|
|
357
|
+
origin,
|
|
358
|
+
ray,
|
|
359
|
+
progressHint,
|
|
360
|
+
low,
|
|
361
|
+
high,
|
|
362
|
+
kind,
|
|
363
|
+
includePitLane,
|
|
364
|
+
refineSteps,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function sampledIndexedTransitionDistance({
|
|
373
|
+
track,
|
|
374
|
+
car,
|
|
375
|
+
origin,
|
|
376
|
+
ray,
|
|
377
|
+
progressHint,
|
|
378
|
+
distance,
|
|
379
|
+
maxDistance,
|
|
380
|
+
kind,
|
|
381
|
+
includePitLane,
|
|
382
|
+
}) {
|
|
383
|
+
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
384
|
+
const firstIndex = Math.max(1, Math.floor(Math.max(0, distance - step * 1.5) / step));
|
|
385
|
+
const lastIndex = Math.ceil(Math.min(maxDistance, distance + step * 8) / step);
|
|
386
|
+
for (let index = firstIndex; index <= lastIndex; index += 1) {
|
|
387
|
+
const high = Math.min(maxDistance, index * step);
|
|
388
|
+
const low = Math.max(0, high - step);
|
|
389
|
+
if (high <= low) continue;
|
|
390
|
+
const previousInside = validatedInsideTrackBorder(track, car, origin, ray, low, progressHint, includePitLane);
|
|
391
|
+
const inside = validatedInsideTrackBorder(track, car, origin, ray, high, progressHint, includePitLane);
|
|
392
|
+
const matched = kind === 'entry'
|
|
393
|
+
? !previousInside && inside
|
|
394
|
+
: previousInside && !inside;
|
|
395
|
+
if (matched) return high;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function refineValidatedTransitionDistance(track, car, origin, ray, progressHint, lowDistance, highDistance, kind, includePitLane, refineSteps) {
|
|
401
|
+
let low = lowDistance;
|
|
402
|
+
let high = highDistance;
|
|
403
|
+
for (let index = 0; index < refineSteps; index += 1) {
|
|
404
|
+
const middle = (low + high) / 2;
|
|
405
|
+
const inside = validatedInsideTrackBorder(track, car, origin, ray, middle, progressHint, includePitLane);
|
|
406
|
+
if (kind === 'entry') {
|
|
407
|
+
if (inside) high = middle;
|
|
408
|
+
else low = middle;
|
|
409
|
+
} else if (inside) {
|
|
410
|
+
low = middle;
|
|
411
|
+
} else {
|
|
412
|
+
high = middle;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return high;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function validatedInsideTrackBorder(track, car, origin, ray, distance, progressHint, includePitLane) {
|
|
419
|
+
const state = nearestTrackState(track, pointOnRay(origin, ray, distance), progressHint, {
|
|
420
|
+
allowPitOverride: pitOverrideAllowedForCar(car),
|
|
421
|
+
indexMode: 'legacy',
|
|
422
|
+
});
|
|
423
|
+
return isInsideTrackBorder(state, track, includePitLane);
|
|
424
|
+
}
|