@inventure71/paddockjs 2.0.0 → 3.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.
- package/CHANGELOG.md +37 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +92 -17
- package/docs/architecture.md +26 -16
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +7 -5
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +121 -33
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +6 -6
- package/docs/rules.md +39 -9
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +76 -20
- package/docs/training.md +27 -20
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +75 -36
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +190 -68
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/restartOptions.js +34 -0
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +34 -5
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +17 -4
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/options.js +1 -1
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +65 -25
- 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 +84 -12
- package/src/environment/sensors/trackRays.js +173 -15
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +112 -9
- package/src/index.js +26 -33
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- 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/pitRouting.js +2 -4
- 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/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +2 -2
- 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/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +193 -434
- 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/track/trackStatePolicy.js +0 -1
- 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 +5 -4
- package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- package/src/ui/timingTowerTemplate.js +29 -12
|
@@ -182,7 +182,9 @@ export function applyEnvironmentPlacements(sim, track, carsById, placements = {}
|
|
|
182
182
|
Object.entries(placements).forEach(([driverId, placement]) => {
|
|
183
183
|
const currentCar = carsById.get(driverId);
|
|
184
184
|
if (!currentCar) return;
|
|
185
|
-
states[driverId] = placementToCarState(track, currentCar, placement
|
|
185
|
+
states[driverId] = placementToCarState(track, currentCar, placement, {
|
|
186
|
+
physicsMode: sim.physicsMode,
|
|
187
|
+
});
|
|
186
188
|
});
|
|
187
189
|
if (typeof sim.setCarStates === 'function') {
|
|
188
190
|
sim.setCarStates(states);
|
|
@@ -193,13 +195,14 @@ export function applyEnvironmentPlacements(sim, track, carsById, placements = {}
|
|
|
193
195
|
});
|
|
194
196
|
}
|
|
195
197
|
|
|
196
|
-
function placementToCarState(track, currentCar, placement) {
|
|
198
|
+
function placementToCarState(track, currentCar, placement, { physicsMode = 'arcade' } = {}) {
|
|
197
199
|
const currentDistanceMeters = currentCar.distanceMeters ?? simUnitsToMeters(currentCar.raceDistance ?? currentCar.progress ?? 0);
|
|
198
200
|
const raceDistance = metersToSimUnits(Math.max(0, placement.distanceMeters ?? currentDistanceMeters));
|
|
199
201
|
const offset = metersToSimUnits(placement.offsetMeters ?? 0);
|
|
200
202
|
const base = pointAt(track, raceDistance);
|
|
201
203
|
const position = offsetTrackPoint(base, offset);
|
|
202
204
|
const heading = normalizeAngle(base.heading + (placement.headingErrorRadians ?? 0));
|
|
205
|
+
const speed = placement.speedKph == null ? null : kphToSimSpeed(placement.speedKph);
|
|
203
206
|
const partial = {
|
|
204
207
|
x: position.x,
|
|
205
208
|
y: position.y,
|
|
@@ -217,11 +220,27 @@ function placementToCarState(track, currentCar, placement) {
|
|
|
217
220
|
canAttack: true,
|
|
218
221
|
throttle: 0,
|
|
219
222
|
brake: 0,
|
|
223
|
+
longitudinalAcceleration: 0,
|
|
224
|
+
lateralAcceleration: 0,
|
|
225
|
+
lateralG: 0,
|
|
226
|
+
longitudinalG: 0,
|
|
227
|
+
gripUsage: 0,
|
|
228
|
+
slipAngleRadians: 0,
|
|
229
|
+
tractionLimited: false,
|
|
220
230
|
steeringAngle: 0,
|
|
221
231
|
yawRate: 0,
|
|
222
232
|
stabilityState: 'stable',
|
|
223
233
|
};
|
|
224
|
-
if (
|
|
234
|
+
if (speed != null) {
|
|
235
|
+
partial.speed = speed;
|
|
236
|
+
if (physicsMode === 'advanced') {
|
|
237
|
+
partial.velocityX = Math.cos(heading) * speed;
|
|
238
|
+
partial.velocityY = Math.sin(heading) * speed;
|
|
239
|
+
} else {
|
|
240
|
+
partial.velocityX = null;
|
|
241
|
+
partial.velocityY = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
225
244
|
return partial;
|
|
226
245
|
}
|
|
227
246
|
|
|
@@ -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,7 +43,10 @@ export function estimateSurfaceHits(
|
|
|
43
43
|
return misses;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const originState = context?.
|
|
46
|
+
const originState = context?.precision === precision
|
|
47
|
+
? context.originState
|
|
48
|
+
: nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
|
|
49
|
+
const originHits = surfaceHitsAtOrigin(requested, originState);
|
|
47
50
|
const indexedHits = estimateIndexedSurfaceHits({
|
|
48
51
|
car,
|
|
49
52
|
track: snapshot.track,
|
|
@@ -52,9 +55,10 @@ export function estimateSurfaceHits(
|
|
|
52
55
|
vector,
|
|
53
56
|
requested,
|
|
54
57
|
originState,
|
|
58
|
+
precision,
|
|
55
59
|
sharedRayQuery,
|
|
56
60
|
});
|
|
57
|
-
if (indexedHits) return { ...misses, ...indexedHits };
|
|
61
|
+
if (indexedHits) return { ...misses, ...indexedHits, ...originHits };
|
|
58
62
|
const analyticHits = estimateAnalyticSurfaceHits({
|
|
59
63
|
car,
|
|
60
64
|
track: snapshot.track,
|
|
@@ -63,16 +67,21 @@ export function estimateSurfaceHits(
|
|
|
63
67
|
requested,
|
|
64
68
|
originState,
|
|
65
69
|
});
|
|
66
|
-
if (analyticHits) return { ...misses, ...analyticHits };
|
|
70
|
+
if (analyticHits) return { ...misses, ...analyticHits, ...originHits };
|
|
67
71
|
|
|
68
72
|
const pending = new Set(requested);
|
|
73
|
+
Object.keys(originHits).forEach((channel) => {
|
|
74
|
+
misses[channel] = originHits[channel];
|
|
75
|
+
pending.delete(channel);
|
|
76
|
+
});
|
|
77
|
+
if (pending.size === 0) return misses;
|
|
69
78
|
const maxDistance = metersToSimUnits(ray.lengthMeters);
|
|
70
79
|
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
71
80
|
let previousDistance = 0;
|
|
72
81
|
let previousState = null;
|
|
73
82
|
|
|
74
83
|
for (let distance = 0; distance <= maxDistance; distance += step) {
|
|
75
|
-
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress);
|
|
84
|
+
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress, precision);
|
|
76
85
|
for (const channel of [...pending]) {
|
|
77
86
|
if (!matchesSurfaceChannel(channel, state)) continue;
|
|
78
87
|
const hitDistance = previousState
|
|
@@ -93,6 +102,19 @@ export function estimateSurfaceHits(
|
|
|
93
102
|
return misses;
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
function surfaceHitsAtOrigin(requested, originState) {
|
|
106
|
+
const hits = {};
|
|
107
|
+
requested.forEach((channel) => {
|
|
108
|
+
if (!matchesSurfaceChannel(channel, originState)) return;
|
|
109
|
+
hits[channel] = {
|
|
110
|
+
hit: true,
|
|
111
|
+
distanceMeters: 0,
|
|
112
|
+
surface: originState.surface ?? null,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
return hits;
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState }) {
|
|
97
119
|
if (!originState || car.inPitLane || originState.inPitLane) return null;
|
|
98
120
|
if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
|
|
@@ -138,10 +160,9 @@ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, origi
|
|
|
138
160
|
return hits;
|
|
139
161
|
}
|
|
140
162
|
|
|
141
|
-
function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, sharedRayQuery }) {
|
|
163
|
+
function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery }) {
|
|
142
164
|
if (!originState || car.inPitLane || originState.inPitLane) return null;
|
|
143
165
|
if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
|
|
144
|
-
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
|
|
145
166
|
|
|
146
167
|
const trackHalfWidth = track.width / 2;
|
|
147
168
|
const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
|
|
@@ -167,10 +188,32 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
|
|
|
167
188
|
const boundaryDistance = channel === 'kerb'
|
|
168
189
|
? minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
|
|
169
190
|
: boundaries.kerbOuterDistance;
|
|
170
|
-
if (boundaryDistance == null)
|
|
191
|
+
if (boundaryDistance == null) {
|
|
192
|
+
if (usesMainTrackOnlyRays(car) && channel === 'illegalSurface') {
|
|
193
|
+
hits[channel] = createSurfaceMiss(ray.lengthMeters);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (canSampleRecoverySurface(track, originState, vector)) return null;
|
|
197
|
+
hits[channel] = createSurfaceMiss(ray.lengthMeters);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const hitDistance = usesMainTrackOnlyRays(car) || refineStepsForPrecision(precision) <= 0 || precision === 'debug'
|
|
201
|
+
? boundaryDistance
|
|
202
|
+
: sampledIndexedSurfaceDistance({
|
|
203
|
+
track,
|
|
204
|
+
car,
|
|
205
|
+
origin,
|
|
206
|
+
vector,
|
|
207
|
+
progressHint: car.progress,
|
|
208
|
+
distance: boundaryDistance,
|
|
209
|
+
maxDistance: metersToSimUnits(ray.lengthMeters),
|
|
210
|
+
channel,
|
|
211
|
+
precision,
|
|
212
|
+
});
|
|
213
|
+
if (hitDistance == null) return null;
|
|
171
214
|
hits[channel] = {
|
|
172
215
|
hit: true,
|
|
173
|
-
distanceMeters: simUnitsToMeters(
|
|
216
|
+
distanceMeters: simUnitsToMeters(hitDistance),
|
|
174
217
|
surface: channel === 'kerb'
|
|
175
218
|
? 'kerb'
|
|
176
219
|
: surfaceBeyondKerb(track, originState.signedOffset ?? 0, lateral, metersToSimUnits(ray.lengthMeters)),
|
|
@@ -180,6 +223,28 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
|
|
|
180
223
|
return hits;
|
|
181
224
|
}
|
|
182
225
|
|
|
226
|
+
function sampledIndexedSurfaceDistance({
|
|
227
|
+
track,
|
|
228
|
+
car,
|
|
229
|
+
origin,
|
|
230
|
+
vector,
|
|
231
|
+
progressHint,
|
|
232
|
+
distance,
|
|
233
|
+
maxDistance,
|
|
234
|
+
channel,
|
|
235
|
+
precision,
|
|
236
|
+
}) {
|
|
237
|
+
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
238
|
+
const firstIndex = Math.max(0, Math.floor(Math.max(0, distance - step * 1.5) / step));
|
|
239
|
+
const lastIndex = Math.ceil(Math.min(maxDistance, distance + step * 8) / step);
|
|
240
|
+
for (let index = firstIndex; index <= lastIndex; index += 1) {
|
|
241
|
+
const sampleDistance = Math.min(maxDistance, index * step);
|
|
242
|
+
const state = nearestRayTrackState(track, car, pointOnRay(origin, vector, sampleDistance), progressHint, precision);
|
|
243
|
+
if (matchesSurfaceChannel(channel, state)) return sampleDistance;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
183
248
|
function usesMainTrackOnlyRays(car) {
|
|
184
249
|
return car?.interaction?.profile === 'batch-training';
|
|
185
250
|
}
|
|
@@ -281,12 +346,19 @@ function matchesSurfaceChannel(channel, state) {
|
|
|
281
346
|
return false;
|
|
282
347
|
}
|
|
283
348
|
|
|
284
|
-
function
|
|
285
|
-
|
|
349
|
+
function canSampleRecoverySurface(track, originState, vector) {
|
|
350
|
+
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
|
|
351
|
+
const kerbOuter = track.width / 2 + (track.kerbWidth ?? 0);
|
|
352
|
+
const offset = originState.signedOffset ?? 0;
|
|
353
|
+
if (Math.abs(offset) <= kerbOuter) return false;
|
|
354
|
+
const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
|
|
355
|
+
if (Math.abs(lateral) < 0.05) return false;
|
|
356
|
+
return offset > 0 ? lateral < 0 : lateral > 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
|
|
286
360
|
return nearestTrackState(track, point, progressHint, {
|
|
287
361
|
allowPitOverride: pitOverrideAllowedForCar(car),
|
|
288
|
-
indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
|
|
289
|
-
hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
|
|
290
362
|
});
|
|
291
363
|
}
|
|
292
364
|
|
|
@@ -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,7 +39,9 @@ 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
46
|
const indexedHit = estimateIndexedTrackHit({
|
|
44
47
|
car,
|
|
@@ -48,9 +51,22 @@ export function estimateTrackHit(
|
|
|
48
51
|
ray,
|
|
49
52
|
lengthMeters,
|
|
50
53
|
includePitLane,
|
|
54
|
+
precision,
|
|
51
55
|
sharedRayQuery,
|
|
52
56
|
});
|
|
53
57
|
if (indexedHit) return indexedHit;
|
|
58
|
+
const directionalAnalyticHit = canUseDirectionalAnalyticTrackHit({ car, track: snapshot.track, originState, ray, includePitLane })
|
|
59
|
+
? estimateAnalyticMainTrackHit({
|
|
60
|
+
car,
|
|
61
|
+
track: snapshot.track,
|
|
62
|
+
origin,
|
|
63
|
+
originState,
|
|
64
|
+
ray,
|
|
65
|
+
lengthMeters,
|
|
66
|
+
includePitLane,
|
|
67
|
+
})
|
|
68
|
+
: null;
|
|
69
|
+
if (directionalAnalyticHit) return directionalAnalyticHit;
|
|
54
70
|
const analyticHit = estimateAnalyticMainTrackHit({
|
|
55
71
|
car,
|
|
56
72
|
track: snapshot.track,
|
|
@@ -65,7 +81,7 @@ export function estimateTrackHit(
|
|
|
65
81
|
let previousInside = null;
|
|
66
82
|
|
|
67
83
|
for (let distance = 0; distance <= maxDistance; distance += step) {
|
|
68
|
-
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, ray, distance), car.progress);
|
|
84
|
+
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, ray, distance), car.progress, precision);
|
|
69
85
|
const inside = isInsideTrackBorder(state, snapshot.track, includePitLane);
|
|
70
86
|
if (previousInside == null) {
|
|
71
87
|
previousInside = inside;
|
|
@@ -86,6 +102,7 @@ export function estimateTrackHit(
|
|
|
86
102
|
kind,
|
|
87
103
|
includePitLane,
|
|
88
104
|
refineStepsForPrecision(precision),
|
|
105
|
+
precision,
|
|
89
106
|
);
|
|
90
107
|
return {
|
|
91
108
|
hit: true,
|
|
@@ -138,9 +155,17 @@ function estimateAnalyticMainTrackHit({ car, track, origin, originState, ray, le
|
|
|
138
155
|
};
|
|
139
156
|
}
|
|
140
157
|
|
|
141
|
-
function
|
|
158
|
+
function canUseDirectionalAnalyticTrackHit({ car, track, originState, ray, includePitLane }) {
|
|
159
|
+
if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return false;
|
|
160
|
+
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
|
|
161
|
+
if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return false;
|
|
162
|
+
|
|
163
|
+
const lateral = ray.x * originState.normalX + ray.y * originState.normalY;
|
|
164
|
+
return Math.abs(lateral) >= 0.08;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane, precision, sharedRayQuery }) {
|
|
142
168
|
if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return null;
|
|
143
|
-
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
|
|
144
169
|
const trackHalfWidth = track.width / 2;
|
|
145
170
|
const inside = Math.abs(originState.signedOffset ?? 0) <= trackHalfWidth;
|
|
146
171
|
const boundaries = findIndexedTrackBandBoundaries(
|
|
@@ -153,11 +178,33 @@ function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthM
|
|
|
153
178
|
sharedRayQuery,
|
|
154
179
|
);
|
|
155
180
|
if (!boundaries.available) return null;
|
|
156
|
-
if (boundaries.trackEdgeDistance == null)
|
|
181
|
+
if (boundaries.trackEdgeDistance == null) {
|
|
182
|
+
if (usesMainTrackOnlyRays(car)) return createTrackMiss(lengthMeters);
|
|
183
|
+
return canSampleRecoveryTransition(track, originState, ray)
|
|
184
|
+
? null
|
|
185
|
+
: createTrackMiss(lengthMeters);
|
|
186
|
+
}
|
|
187
|
+
const kind = inside ? 'exit' : 'entry';
|
|
188
|
+
const hitDistance = usesMainTrackOnlyRays(car)
|
|
189
|
+
? boundaries.trackEdgeDistance
|
|
190
|
+
: validatedIndexedTransitionDistance({
|
|
191
|
+
track,
|
|
192
|
+
car,
|
|
193
|
+
origin,
|
|
194
|
+
ray,
|
|
195
|
+
progressHint: car.progress,
|
|
196
|
+
distance: boundaries.trackEdgeDistance,
|
|
197
|
+
maxDistance: metersToSimUnits(lengthMeters),
|
|
198
|
+
kind,
|
|
199
|
+
includePitLane,
|
|
200
|
+
precision,
|
|
201
|
+
originInside: inside,
|
|
202
|
+
});
|
|
203
|
+
if (hitDistance == null) return null;
|
|
157
204
|
return {
|
|
158
205
|
hit: true,
|
|
159
|
-
distanceMeters: simUnitsToMeters(
|
|
160
|
-
kind
|
|
206
|
+
distanceMeters: simUnitsToMeters(hitDistance),
|
|
207
|
+
kind,
|
|
161
208
|
};
|
|
162
209
|
}
|
|
163
210
|
|
|
@@ -223,12 +270,13 @@ function refineTrackTransitionDistance(
|
|
|
223
270
|
kind,
|
|
224
271
|
includePitLane = true,
|
|
225
272
|
refineSteps = TRACK_RAY_REFINE_STEPS,
|
|
273
|
+
precision = 'debug',
|
|
226
274
|
) {
|
|
227
275
|
let low = lowDistance;
|
|
228
276
|
let high = highDistance;
|
|
229
277
|
for (let index = 0; index < refineSteps; index += 1) {
|
|
230
278
|
const middle = (low + high) / 2;
|
|
231
|
-
const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint);
|
|
279
|
+
const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint, precision);
|
|
232
280
|
const inside = isInsideTrackBorder(state, track, includePitLane);
|
|
233
281
|
if (kind === 'entry') {
|
|
234
282
|
if (inside) high = middle;
|
|
@@ -242,12 +290,9 @@ function refineTrackTransitionDistance(
|
|
|
242
290
|
return high;
|
|
243
291
|
}
|
|
244
292
|
|
|
245
|
-
function nearestRayTrackState(track, car, point, progressHint) {
|
|
246
|
-
const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
|
|
293
|
+
function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
|
|
247
294
|
return nearestTrackState(track, point, progressHint, {
|
|
248
295
|
allowPitOverride: pitOverrideAllowedForCar(car),
|
|
249
|
-
indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
|
|
250
|
-
hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
|
|
251
296
|
});
|
|
252
297
|
}
|
|
253
298
|
|
|
@@ -259,3 +304,116 @@ function isInsideTrackBorder(state, track, includePitLane = true) {
|
|
|
259
304
|
if (includePitLane && state.inPitLane) return true;
|
|
260
305
|
return state.crossTrackError <= track.width / 2;
|
|
261
306
|
}
|
|
307
|
+
|
|
308
|
+
function canSampleRecoveryTransition(track, originState, ray) {
|
|
309
|
+
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
|
|
310
|
+
const trackHalfWidth = track.width / 2;
|
|
311
|
+
const offset = originState.signedOffset ?? 0;
|
|
312
|
+
if (Math.abs(offset) <= trackHalfWidth) return false;
|
|
313
|
+
const lateral = ray.x * originState.normalX + ray.y * originState.normalY;
|
|
314
|
+
if (Math.abs(lateral) < 0.05) return false;
|
|
315
|
+
return offset > 0 ? lateral < 0 : lateral > 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function validatedIndexedTransitionDistance({
|
|
319
|
+
track,
|
|
320
|
+
car,
|
|
321
|
+
origin,
|
|
322
|
+
ray,
|
|
323
|
+
progressHint,
|
|
324
|
+
distance,
|
|
325
|
+
maxDistance,
|
|
326
|
+
kind,
|
|
327
|
+
includePitLane,
|
|
328
|
+
precision = 'driver',
|
|
329
|
+
originInside = kind === 'exit',
|
|
330
|
+
}) {
|
|
331
|
+
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
332
|
+
const refineSteps = refineStepsForPrecision(precision);
|
|
333
|
+
if (refineSteps <= 0) {
|
|
334
|
+
return distance;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const windows = [step * 1.5, step * 8];
|
|
338
|
+
|
|
339
|
+
for (const searchWindow of windows) {
|
|
340
|
+
const low = Math.max(0, distance - searchWindow);
|
|
341
|
+
const high = Math.min(maxDistance, distance + searchWindow);
|
|
342
|
+
if (high <= low) continue;
|
|
343
|
+
const previousInside = validatedInsideTrackBorder(track, car, origin, ray, low, progressHint, includePitLane);
|
|
344
|
+
const inside = validatedInsideTrackBorder(track, car, origin, ray, high, progressHint, includePitLane);
|
|
345
|
+
const matched = kind === 'entry'
|
|
346
|
+
? !previousInside && inside
|
|
347
|
+
: previousInside && !inside;
|
|
348
|
+
if (matched) {
|
|
349
|
+
if (refineSteps <= 0) return distance;
|
|
350
|
+
return refineValidatedTransitionDistance(
|
|
351
|
+
track,
|
|
352
|
+
car,
|
|
353
|
+
origin,
|
|
354
|
+
ray,
|
|
355
|
+
progressHint,
|
|
356
|
+
low,
|
|
357
|
+
high,
|
|
358
|
+
kind,
|
|
359
|
+
includePitLane,
|
|
360
|
+
refineSteps,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function sampledIndexedTransitionDistance({
|
|
369
|
+
track,
|
|
370
|
+
car,
|
|
371
|
+
origin,
|
|
372
|
+
ray,
|
|
373
|
+
progressHint,
|
|
374
|
+
distance,
|
|
375
|
+
maxDistance,
|
|
376
|
+
kind,
|
|
377
|
+
includePitLane,
|
|
378
|
+
}) {
|
|
379
|
+
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
380
|
+
const firstIndex = Math.max(1, Math.floor(Math.max(0, distance - step * 1.5) / step));
|
|
381
|
+
const lastIndex = Math.ceil(Math.min(maxDistance, distance + step * 8) / step);
|
|
382
|
+
for (let index = firstIndex; index <= lastIndex; index += 1) {
|
|
383
|
+
const high = Math.min(maxDistance, index * step);
|
|
384
|
+
const low = Math.max(0, high - step);
|
|
385
|
+
if (high <= low) continue;
|
|
386
|
+
const previousInside = validatedInsideTrackBorder(track, car, origin, ray, low, progressHint, includePitLane);
|
|
387
|
+
const inside = validatedInsideTrackBorder(track, car, origin, ray, high, progressHint, includePitLane);
|
|
388
|
+
const matched = kind === 'entry'
|
|
389
|
+
? !previousInside && inside
|
|
390
|
+
: previousInside && !inside;
|
|
391
|
+
if (matched) return high;
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function refineValidatedTransitionDistance(track, car, origin, ray, progressHint, lowDistance, highDistance, kind, includePitLane, refineSteps) {
|
|
397
|
+
let low = lowDistance;
|
|
398
|
+
let high = highDistance;
|
|
399
|
+
for (let index = 0; index < refineSteps; index += 1) {
|
|
400
|
+
const middle = (low + high) / 2;
|
|
401
|
+
const inside = validatedInsideTrackBorder(track, car, origin, ray, middle, progressHint, includePitLane);
|
|
402
|
+
if (kind === 'entry') {
|
|
403
|
+
if (inside) high = middle;
|
|
404
|
+
else low = middle;
|
|
405
|
+
} else if (inside) {
|
|
406
|
+
low = middle;
|
|
407
|
+
} else {
|
|
408
|
+
high = middle;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return high;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function validatedInsideTrackBorder(track, car, origin, ray, distance, progressHint, includePitLane) {
|
|
415
|
+
const state = nearestTrackState(track, pointOnRay(origin, ray, distance), progressHint, {
|
|
416
|
+
allowPitOverride: pitOverrideAllowedForCar(car),
|
|
417
|
+
});
|
|
418
|
+
return isInsideTrackBorder(state, track, includePitLane);
|
|
419
|
+
}
|