@inventure71/paddockjs 0.3.0 → 2.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 +91 -0
- package/INSTALL_AND_UPDATE.md +5 -2
- package/README.md +328 -13
- package/docs/architecture.md +150 -41
- package/docs/concepts.md +22 -3
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +474 -34
- package/docs/data_model.md +1 -1
- package/docs/index.md +1 -0
- package/docs/learnings.md +23 -1
- package/docs/rules.md +169 -21
- package/docs/system_specs.md +56 -20
- package/docs/training.md +369 -25
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +15 -4
- package/src/api/PaddockSimulatorController.js +36 -2
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +615 -940
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +14 -1
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +24 -4
- package/src/environment/actions.js +38 -7
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +206 -0
- package/src/environment/index.d.ts +638 -6
- package/src/environment/index.js +14 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +33 -0
- package/src/environment/observations.js +500 -43
- package/src/environment/options.js +41 -2
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +260 -24
- package/src/environment/scenarios.js +249 -0
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +65 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -257
- package/src/environment/specs.js +188 -9
- package/src/environment/workerProtocol.js +64 -0
- package/src/index.d.ts +533 -7
- package/src/index.js +26 -0
- package/src/rendering/proceduralTrackAsset.js +20 -510
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +214 -0
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -253
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +92 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +386 -1012
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/collisionSteward.js +69 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyLedger.js +153 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +141 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +34 -0
- package/src/simulation/snapshots/raceSnapshots.js +192 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryIndex.js +945 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -628
- package/src/simulation/units.js +6 -4
- package/src/simulation/vehicle/contactResolution.js +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -0
- package/src/simulation/vehicleGeometry.js +1 -0
- package/src/simulation/vehiclePhysics.js +1 -104
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -0
- package/src/styles.css +342 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +20 -426
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +6 -5
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/templateUtils.js +23 -0
- package/src/ui/timingTowerTemplate.js +34 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
|
|
2
|
+
import { normalizeRelativeHeading } from './rayGeometry.js';
|
|
3
|
+
import { nearbyDetectableTargets } from './sensorTargets.js';
|
|
4
|
+
|
|
5
|
+
export function buildNearbyCars(car, snapshot, { maxCars = 6, radiusMeters = 150 } = {}) {
|
|
6
|
+
const limit = Math.max(0, Math.floor(maxCars));
|
|
7
|
+
if (limit === 0) return [];
|
|
8
|
+
const radius = metersToSimUnits(radiusMeters);
|
|
9
|
+
const radiusSquared = radius * radius;
|
|
10
|
+
const forwardX = Math.cos(car.heading);
|
|
11
|
+
const forwardY = Math.sin(car.heading);
|
|
12
|
+
const rightX = -Math.sin(car.heading);
|
|
13
|
+
const rightY = Math.cos(car.heading);
|
|
14
|
+
const nearest = [];
|
|
15
|
+
|
|
16
|
+
nearbyDetectableTargets(car, snapshot).forEach((other) => {
|
|
17
|
+
const dx = other.x - car.x;
|
|
18
|
+
const dy = other.y - car.y;
|
|
19
|
+
const distanceSquared = dx * dx + dy * dy;
|
|
20
|
+
if (distanceSquared > radiusSquared) return;
|
|
21
|
+
|
|
22
|
+
const forward = forwardX * dx + forwardY * dy;
|
|
23
|
+
const right = rightX * dx + rightY * dy;
|
|
24
|
+
const entry = {
|
|
25
|
+
id: other.id,
|
|
26
|
+
relativeForwardMeters: simUnitsToMeters(forward),
|
|
27
|
+
relativeRightMeters: simUnitsToMeters(right),
|
|
28
|
+
relativeDistanceMeters: simUnitsToMeters(Math.sqrt(distanceSquared)),
|
|
29
|
+
relativeSpeedKph: other.speedKph - car.speedKph,
|
|
30
|
+
relativeHeadingRadians: normalizeRelativeHeading(other.heading - car.heading),
|
|
31
|
+
ahead: forward > 0,
|
|
32
|
+
sameLap: other.lap != null && other.lap === car.lap,
|
|
33
|
+
entityType: other.entityType,
|
|
34
|
+
distanceSquared,
|
|
35
|
+
order: other.order,
|
|
36
|
+
};
|
|
37
|
+
insertNearestCar(nearest, entry, limit);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return nearest.map(({ distanceSquared, order, ...entry }) => entry);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function insertNearestCar(nearest, entry, limit) {
|
|
44
|
+
let insertAt = nearest.findIndex((candidate) => (
|
|
45
|
+
entry.distanceSquared < candidate.distanceSquared ||
|
|
46
|
+
(entry.distanceSquared === candidate.distanceSquared && entry.order < candidate.order)
|
|
47
|
+
));
|
|
48
|
+
if (insertAt < 0) insertAt = nearest.length;
|
|
49
|
+
if (insertAt >= limit) return;
|
|
50
|
+
nearest.splice(insertAt, 0, entry);
|
|
51
|
+
if (nearest.length > limit) nearest.pop();
|
|
52
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { kphToSimSpeed, simSpeedToMetersPerSecond, simUnitsToMeters } from '../../simulation/units.js';
|
|
2
|
+
import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
|
|
3
|
+
|
|
4
|
+
export function enrichOpponentRadar(car, nearbyCars, snapshot) {
|
|
5
|
+
const targetsById = new Map([
|
|
6
|
+
...(snapshot.cars ?? []).map((entry) => [entry.id, entry]),
|
|
7
|
+
...(snapshot.replayGhosts ?? []).map((entry) => [entry.id, entry]),
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
return nearbyCars.map((nearby) => {
|
|
11
|
+
const target = targetsById.get(nearby.id);
|
|
12
|
+
const derived = target ? deriveRadarFields(car, target, nearby) : fallbackRadarFields(nearby);
|
|
13
|
+
return {
|
|
14
|
+
...nearby,
|
|
15
|
+
...derived,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function deriveRadarFields(car, target, nearby) {
|
|
21
|
+
const dx = (target.x ?? 0) - (car.x ?? 0);
|
|
22
|
+
const dy = (target.y ?? 0) - (car.y ?? 0);
|
|
23
|
+
const distance = Math.max(1e-9, Math.hypot(dx, dy));
|
|
24
|
+
const selfVelocity = velocity(car);
|
|
25
|
+
const targetVelocity = velocity(target);
|
|
26
|
+
const relativeVelocity = {
|
|
27
|
+
x: targetVelocity.x - selfVelocity.x,
|
|
28
|
+
y: targetVelocity.y - selfVelocity.y,
|
|
29
|
+
};
|
|
30
|
+
const closingRate = -((relativeVelocity.x * dx + relativeVelocity.y * dy) / distance);
|
|
31
|
+
const closingRateMetersPerSecond = simSpeedToMetersPerSecond(closingRate);
|
|
32
|
+
const distanceMeters = nearby.relativeDistanceMeters ?? simUnitsToMeters(distance);
|
|
33
|
+
const longitudinalOverlap = Math.abs(nearby.relativeForwardMeters ?? 0) <= simUnitsToMeters(VEHICLE_GEOMETRY.bodyLength);
|
|
34
|
+
const lateralOverlap = Math.abs(nearby.relativeRightMeters ?? 0) <= simUnitsToMeters(VEHICLE_GEOMETRY.bodyWidth);
|
|
35
|
+
const sideOverlap = longitudinalOverlap && lateralOverlap;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
behind: !nearby.ahead,
|
|
39
|
+
closingRateMetersPerSecond,
|
|
40
|
+
timeToContactSeconds: closingRateMetersPerSecond > 0
|
|
41
|
+
? distanceMeters / closingRateMetersPerSecond
|
|
42
|
+
: null,
|
|
43
|
+
leftOverlap: sideOverlap && (nearby.relativeRightMeters ?? 0) < 0,
|
|
44
|
+
rightOverlap: sideOverlap && (nearby.relativeRightMeters ?? 0) > 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fallbackRadarFields(nearby) {
|
|
49
|
+
return {
|
|
50
|
+
behind: !nearby.ahead,
|
|
51
|
+
closingRateMetersPerSecond: 0,
|
|
52
|
+
timeToContactSeconds: null,
|
|
53
|
+
leftOverlap: false,
|
|
54
|
+
rightOverlap: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function velocity(car) {
|
|
59
|
+
const speed = Number.isFinite(car.speed) ? car.speed : kphToSimSpeed(car.speedKph ?? 0);
|
|
60
|
+
const heading = car.heading ?? car.headingRadians ?? 0;
|
|
61
|
+
return {
|
|
62
|
+
x: Math.cos(heading) * speed,
|
|
63
|
+
y: Math.sin(heading) * speed,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { DEFAULT_RAY_ANGLES_DEGREES } from './rayDefaults.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_RAY_LENGTH_METERS = 120;
|
|
4
|
+
|
|
5
|
+
export const RAY_CHANNELS = Object.freeze([
|
|
6
|
+
'roadEdge',
|
|
7
|
+
'kerb',
|
|
8
|
+
'illegalSurface',
|
|
9
|
+
'car',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const RAY_PRECISION_MODES = Object.freeze(['driver', 'debug']);
|
|
13
|
+
|
|
14
|
+
export const RAY_LAYOUT_PRESETS = Object.freeze({
|
|
15
|
+
compact: DEFAULT_RAY_ANGLES_DEGREES.map((angleDegrees) => ({ angleDegrees })),
|
|
16
|
+
'driver-front-heavy': [
|
|
17
|
+
-140, -100, -70, -50, -35, -20, -10, 0, 10, 20, 35, 50, 70, 100, 140, 180,
|
|
18
|
+
].map((angleDegrees) => ({
|
|
19
|
+
angleDegrees,
|
|
20
|
+
lengthMeters: Math.abs(angleDegrees) <= 70 ? 240 : Math.abs(angleDegrees) <= 100 ? 90 : 60,
|
|
21
|
+
})),
|
|
22
|
+
'lidar-lite': Array.from({ length: 25 }, (_, index) => ({
|
|
23
|
+
angleDegrees: -120 + index * 10,
|
|
24
|
+
lengthMeters: Math.abs(-120 + index * 10) <= 70 ? 260 : 100,
|
|
25
|
+
})),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export function normalizeRayOptions(rayOptions = {}) {
|
|
29
|
+
const defaultLengthMeters = positiveNumber(
|
|
30
|
+
rayOptions.defaultLengthMeters ?? rayOptions.lengthMeters,
|
|
31
|
+
DEFAULT_RAY_LENGTH_METERS,
|
|
32
|
+
);
|
|
33
|
+
const rays = normalizeRays(rayOptions, defaultLengthMeters);
|
|
34
|
+
const channels = normalizeRayChannels(rayOptions);
|
|
35
|
+
const precision = RAY_PRECISION_MODES.includes(rayOptions.precision) ? rayOptions.precision : 'driver';
|
|
36
|
+
const anglesDegrees = rays.map((ray) => ray.angleDegrees);
|
|
37
|
+
const lengthMeters = rays.length ? Math.max(...rays.map((ray) => ray.lengthMeters)) : defaultLengthMeters;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...rayOptions,
|
|
41
|
+
enabled: rayOptions.enabled !== false,
|
|
42
|
+
anglesDegrees,
|
|
43
|
+
lengthMeters,
|
|
44
|
+
defaultLengthMeters,
|
|
45
|
+
rays,
|
|
46
|
+
channels,
|
|
47
|
+
precision,
|
|
48
|
+
detectTrack: channels.includes('roadEdge'),
|
|
49
|
+
detectCars: channels.includes('car'),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeRays(rayOptions, defaultLengthMeters) {
|
|
54
|
+
const configuredRays = Array.isArray(rayOptions.rays)
|
|
55
|
+
? rayOptions.rays
|
|
56
|
+
: resolvePresetRays(rayOptions.layout);
|
|
57
|
+
const source = configuredRays.length
|
|
58
|
+
? configuredRays
|
|
59
|
+
: normalizeAngles(rayOptions.anglesDegrees).map((angleDegrees) => ({ angleDegrees }));
|
|
60
|
+
|
|
61
|
+
return source.map((ray, index) => {
|
|
62
|
+
const angleDegrees = finiteNumber(
|
|
63
|
+
typeof ray === 'number' ? ray : ray?.angleDegrees,
|
|
64
|
+
DEFAULT_RAY_ANGLES_DEGREES[index % DEFAULT_RAY_ANGLES_DEGREES.length],
|
|
65
|
+
);
|
|
66
|
+
const lengthMeters = positiveNumber(
|
|
67
|
+
typeof ray === 'object' ? ray.lengthMeters : null,
|
|
68
|
+
defaultLengthMeters,
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
id: typeof ray === 'object' && ray.id ? String(ray.id) : `ray-${index}`,
|
|
72
|
+
angleDegrees,
|
|
73
|
+
lengthMeters,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolvePresetRays(layout) {
|
|
79
|
+
if (typeof layout !== 'string') return [];
|
|
80
|
+
return RAY_LAYOUT_PRESETS[layout] ?? [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeAngles(value) {
|
|
84
|
+
if (!Array.isArray(value) || value.length === 0) return [...DEFAULT_RAY_ANGLES_DEGREES];
|
|
85
|
+
const angles = value.map((angle) => Number(angle)).filter((angle) => Number.isFinite(angle));
|
|
86
|
+
return angles.length ? angles : [...DEFAULT_RAY_ANGLES_DEGREES];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeRayChannels(rayOptions) {
|
|
90
|
+
if (Array.isArray(rayOptions.channels) && rayOptions.channels.length) {
|
|
91
|
+
const channelSet = new Set(RAY_CHANNELS);
|
|
92
|
+
const channels = rayOptions.channels.filter((channel) => channelSet.has(channel));
|
|
93
|
+
if (channels.length) return [...new Set(channels)];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const channels = [];
|
|
97
|
+
if (rayOptions.detectTrack !== false) channels.push('roadEdge');
|
|
98
|
+
if (rayOptions.detectCars !== false) channels.push('car');
|
|
99
|
+
return channels;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function positiveNumber(value, fallback) {
|
|
103
|
+
const number = Number(value);
|
|
104
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function finiteNumber(value, fallback) {
|
|
108
|
+
const number = Number(value);
|
|
109
|
+
return Number.isFinite(number) ? number : fallback;
|
|
110
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const TRACK_RAY_STEP_METERS = 1;
|
|
2
|
+
export const TRACK_RAY_REFINE_STEPS = 12;
|
|
3
|
+
export const DRIVER_RAY_REFINE_STEPS = 0;
|
|
4
|
+
export const ANALYTIC_TRACK_RAY_MAX_CURVATURE = 0.00005;
|
|
5
|
+
export const PIT_CONNECTOR_RAY_FALLBACK_METERS = 55;
|
|
6
|
+
export const DEFAULT_RAY_ANGLES_DEGREES = [-135, -60, -20, 0, 20, 60, 135, 180];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function getCarRayOrigin(car) {
|
|
2
|
+
return {
|
|
3
|
+
x: car.x,
|
|
4
|
+
y: car.y,
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getCarRayVector(car, angleDegrees) {
|
|
9
|
+
const angle = car.heading + degreesToRadians(angleDegrees);
|
|
10
|
+
return {
|
|
11
|
+
x: Math.cos(angle),
|
|
12
|
+
y: Math.sin(angle),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function pointOnRay(origin, ray, distance) {
|
|
17
|
+
return {
|
|
18
|
+
x: origin.x + ray.x * distance,
|
|
19
|
+
y: origin.y + ray.y * distance,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function intersectAxisAlignedBoxRay(origin, ray, halfLength, halfWidth) {
|
|
24
|
+
let tMin = -Infinity;
|
|
25
|
+
let tMax = Infinity;
|
|
26
|
+
const xRange = intersectSlab(origin.x, ray.x, -halfLength, halfLength);
|
|
27
|
+
const yRange = intersectSlab(origin.y, ray.y, -halfWidth, halfWidth);
|
|
28
|
+
if (!xRange || !yRange) return null;
|
|
29
|
+
tMin = Math.max(tMin, xRange.min, yRange.min);
|
|
30
|
+
tMax = Math.min(tMax, xRange.max, yRange.max);
|
|
31
|
+
if (tMax < 0 || tMin > tMax) return null;
|
|
32
|
+
return Math.max(0, tMin);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function dot(a, b) {
|
|
36
|
+
return a.x * b.x + a.y * b.y;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function degreesToRadians(degrees) {
|
|
40
|
+
return (degrees * Math.PI) / 180;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeRelativeHeading(angle) {
|
|
44
|
+
let value = angle;
|
|
45
|
+
while (value > Math.PI) value -= Math.PI * 2;
|
|
46
|
+
while (value < -Math.PI) value += Math.PI * 2;
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function intersectSlab(origin, direction, min, max) {
|
|
51
|
+
if (Math.abs(direction) < 1e-9) {
|
|
52
|
+
return origin >= min && origin <= max ? { min: -Infinity, max: Infinity } : null;
|
|
53
|
+
}
|
|
54
|
+
const first = (min - origin) / direction;
|
|
55
|
+
const second = (max - origin) / direction;
|
|
56
|
+
return {
|
|
57
|
+
min: Math.min(first, second),
|
|
58
|
+
max: Math.max(first, second),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { metersToSimUnits } from '../../simulation/units.js';
|
|
2
|
+
|
|
3
|
+
export function canUseLocalStripRayApproximation(track, state) {
|
|
4
|
+
if (!track || !state || state.inPitLane) return false;
|
|
5
|
+
const kerbOuter = track.width / 2 + (track.kerbWidth ?? 0);
|
|
6
|
+
return Number.isFinite(state.crossTrackError) && state.crossTrackError <= kerbOuter;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function canUseIndexedRecoveryRayApproximation(track, state) {
|
|
10
|
+
if (!track || !state || state.inPitLane) return false;
|
|
11
|
+
const recoveryBand = track.width / 2 +
|
|
12
|
+
(track.kerbWidth ?? 0) +
|
|
13
|
+
(track.gravelWidth ?? 0) +
|
|
14
|
+
(track.runoffWidth ?? 0) +
|
|
15
|
+
metersToSimUnits(64);
|
|
16
|
+
return Number.isFinite(state.crossTrackError) && state.crossTrackError <= recoveryBand;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const canUseBatchTrainingRayApproximation = canUseIndexedRecoveryRayApproximation;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNearbyDetectable,
|
|
3
|
+
isRayDetectable,
|
|
4
|
+
} from '../../simulation/participants/participantInteractions.js';
|
|
5
|
+
|
|
6
|
+
export const SENSOR_TARGET_CAR = 'car';
|
|
7
|
+
export const SENSOR_TARGET_REPLAY_GHOST = 'replayGhost';
|
|
8
|
+
|
|
9
|
+
export function rayDetectableTargets(self, snapshot) {
|
|
10
|
+
return rayDetectableTargetsForSnapshot(snapshot).filter((target) => target.id !== self.id);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function nearbyDetectableTargets(self, snapshot) {
|
|
14
|
+
return nearbyDetectableTargetsForSnapshot(snapshot).filter((target) => target.id !== self.id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function rayDetectableTargetsForSnapshot(snapshot) {
|
|
18
|
+
return [
|
|
19
|
+
...detectableCars(snapshot, isRayDetectable),
|
|
20
|
+
...detectableReplayGhosts(snapshot, 'detectableByRays'),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function nearbyDetectableTargetsForSnapshot(snapshot) {
|
|
25
|
+
return [
|
|
26
|
+
...detectableCars(snapshot, isNearbyDetectable),
|
|
27
|
+
...detectableReplayGhosts(snapshot, 'detectableAsNearby'),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function detectableCars(snapshot, predicate) {
|
|
32
|
+
return (snapshot.cars ?? [])
|
|
33
|
+
.filter((car) => predicate(car))
|
|
34
|
+
.map((car, order) => ({
|
|
35
|
+
entityType: SENSOR_TARGET_CAR,
|
|
36
|
+
id: car.id,
|
|
37
|
+
x: car.x,
|
|
38
|
+
y: car.y,
|
|
39
|
+
heading: car.heading,
|
|
40
|
+
speedKph: car.speedKph ?? 0,
|
|
41
|
+
lap: car.lap,
|
|
42
|
+
order,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function detectableReplayGhosts(snapshot, sensorFlag) {
|
|
47
|
+
const carCount = snapshot.cars?.length ?? 0;
|
|
48
|
+
return (snapshot.replayGhosts ?? [])
|
|
49
|
+
.filter((ghost) => ghost.sensors?.[sensorFlag] === true)
|
|
50
|
+
.map((ghost, index) => ({
|
|
51
|
+
entityType: SENSOR_TARGET_REPLAY_GHOST,
|
|
52
|
+
id: ghost.id,
|
|
53
|
+
x: ghost.x,
|
|
54
|
+
y: ghost.y,
|
|
55
|
+
heading: ghost.heading,
|
|
56
|
+
speedKph: ghost.speedKph ?? 0,
|
|
57
|
+
lap: null,
|
|
58
|
+
order: carCount + index,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { nearestTrackState } from '../../simulation/track/trackModel.js';
|
|
2
|
+
import { pitOverrideAllowedForCar } from '../../simulation/track/trackStatePolicy.js';
|
|
3
|
+
import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
|
|
4
|
+
import {
|
|
5
|
+
ANALYTIC_TRACK_RAY_MAX_CURVATURE,
|
|
6
|
+
DRIVER_RAY_REFINE_STEPS,
|
|
7
|
+
PIT_CONNECTOR_RAY_FALLBACK_METERS,
|
|
8
|
+
TRACK_RAY_REFINE_STEPS,
|
|
9
|
+
TRACK_RAY_STEP_METERS,
|
|
10
|
+
} from './rayDefaults.js';
|
|
11
|
+
import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
|
|
12
|
+
import { pointOnRay } from './rayGeometry.js';
|
|
13
|
+
import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
|
|
14
|
+
|
|
15
|
+
const LEGAL_SURFACES = new Set(['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box']);
|
|
16
|
+
const SURFACE_CHANNELS = new Set(['kerb', 'illegalSurface']);
|
|
17
|
+
|
|
18
|
+
export function requestedSurfaceChannels(channels = []) {
|
|
19
|
+
return channels.filter((channel) => SURFACE_CHANNELS.has(channel));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createSurfaceMiss(lengthMeters) {
|
|
23
|
+
return {
|
|
24
|
+
hit: false,
|
|
25
|
+
distanceMeters: lengthMeters,
|
|
26
|
+
surface: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function estimateSurfaceHits(
|
|
31
|
+
car,
|
|
32
|
+
snapshot,
|
|
33
|
+
ray,
|
|
34
|
+
origin,
|
|
35
|
+
vector,
|
|
36
|
+
channels,
|
|
37
|
+
context = null,
|
|
38
|
+
{ precision = 'driver', sharedRayQuery = null } = {},
|
|
39
|
+
) {
|
|
40
|
+
const requested = requestedSurfaceChannels(channels);
|
|
41
|
+
const misses = Object.fromEntries(requested.map((channel) => [channel, createSurfaceMiss(ray.lengthMeters)]));
|
|
42
|
+
if (!requested.length || !Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
|
|
43
|
+
return misses;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const originState = context?.originState ?? nearestRayTrackState(snapshot.track, car, origin, car.progress);
|
|
47
|
+
const indexedHits = estimateIndexedSurfaceHits({
|
|
48
|
+
car,
|
|
49
|
+
track: snapshot.track,
|
|
50
|
+
ray,
|
|
51
|
+
origin,
|
|
52
|
+
vector,
|
|
53
|
+
requested,
|
|
54
|
+
originState,
|
|
55
|
+
sharedRayQuery,
|
|
56
|
+
});
|
|
57
|
+
if (indexedHits) return { ...misses, ...indexedHits };
|
|
58
|
+
const analyticHits = estimateAnalyticSurfaceHits({
|
|
59
|
+
car,
|
|
60
|
+
track: snapshot.track,
|
|
61
|
+
ray,
|
|
62
|
+
vector,
|
|
63
|
+
requested,
|
|
64
|
+
originState,
|
|
65
|
+
});
|
|
66
|
+
if (analyticHits) return { ...misses, ...analyticHits };
|
|
67
|
+
|
|
68
|
+
const pending = new Set(requested);
|
|
69
|
+
const maxDistance = metersToSimUnits(ray.lengthMeters);
|
|
70
|
+
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
71
|
+
let previousDistance = 0;
|
|
72
|
+
let previousState = null;
|
|
73
|
+
|
|
74
|
+
for (let distance = 0; distance <= maxDistance; distance += step) {
|
|
75
|
+
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress);
|
|
76
|
+
for (const channel of [...pending]) {
|
|
77
|
+
if (!matchesSurfaceChannel(channel, state)) continue;
|
|
78
|
+
const hitDistance = previousState
|
|
79
|
+
? refineSurfaceTransition(snapshot.track, car, origin, vector, car.progress, previousDistance, distance, channel, refineStepsForPrecision(precision))
|
|
80
|
+
: distance;
|
|
81
|
+
misses[channel] = {
|
|
82
|
+
hit: true,
|
|
83
|
+
distanceMeters: simUnitsToMeters(hitDistance),
|
|
84
|
+
surface: state.surface ?? null,
|
|
85
|
+
};
|
|
86
|
+
pending.delete(channel);
|
|
87
|
+
}
|
|
88
|
+
if (pending.size === 0) break;
|
|
89
|
+
previousDistance = distance;
|
|
90
|
+
previousState = state;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return misses;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState }) {
|
|
97
|
+
if (!originState || car.inPitLane || originState.inPitLane) return null;
|
|
98
|
+
if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
|
|
99
|
+
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
|
|
100
|
+
if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return null;
|
|
101
|
+
|
|
102
|
+
const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
|
|
103
|
+
if (Math.abs(lateral) < 0.08) return Object.fromEntries(requested.map((channel) => [
|
|
104
|
+
channel,
|
|
105
|
+
createSurfaceMiss(ray.lengthMeters),
|
|
106
|
+
]));
|
|
107
|
+
|
|
108
|
+
const trackHalfWidth = track.width / 2;
|
|
109
|
+
const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
|
|
110
|
+
const maxDistance = metersToSimUnits(ray.lengthMeters);
|
|
111
|
+
const offset = originState.signedOffset ?? car.signedOffset ?? 0;
|
|
112
|
+
const hits = {};
|
|
113
|
+
|
|
114
|
+
requested.forEach((channel) => {
|
|
115
|
+
if (channel === 'kerb') {
|
|
116
|
+
hits[channel] = hitAbsOffsetBand({
|
|
117
|
+
offset,
|
|
118
|
+
lateral,
|
|
119
|
+
minAbsOffset: trackHalfWidth,
|
|
120
|
+
maxAbsOffset: kerbOuter,
|
|
121
|
+
maxDistance,
|
|
122
|
+
lengthMeters: ray.lengthMeters,
|
|
123
|
+
surface: 'kerb',
|
|
124
|
+
});
|
|
125
|
+
} else if (channel === 'illegalSurface') {
|
|
126
|
+
hits[channel] = hitAbsOffsetBand({
|
|
127
|
+
offset,
|
|
128
|
+
lateral,
|
|
129
|
+
minAbsOffset: kerbOuter,
|
|
130
|
+
maxAbsOffset: Infinity,
|
|
131
|
+
maxDistance,
|
|
132
|
+
lengthMeters: ray.lengthMeters,
|
|
133
|
+
surface: surfaceBeyondKerb(track, offset, lateral, maxDistance),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return hits;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, sharedRayQuery }) {
|
|
142
|
+
if (!originState || car.inPitLane || originState.inPitLane) return null;
|
|
143
|
+
if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
|
|
144
|
+
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
|
|
145
|
+
|
|
146
|
+
const trackHalfWidth = track.width / 2;
|
|
147
|
+
const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
|
|
148
|
+
const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
|
|
149
|
+
const boundaries = findIndexedTrackBandBoundaries(
|
|
150
|
+
track,
|
|
151
|
+
origin,
|
|
152
|
+
vector,
|
|
153
|
+
ray.lengthMeters,
|
|
154
|
+
trackHalfWidth,
|
|
155
|
+
kerbOuter,
|
|
156
|
+
sharedRayQuery,
|
|
157
|
+
);
|
|
158
|
+
if (!boundaries.available) return null;
|
|
159
|
+
const hits = {};
|
|
160
|
+
|
|
161
|
+
for (const channel of requested) {
|
|
162
|
+
if (matchesSurfaceChannel(channel, originState)) {
|
|
163
|
+
hits[channel] = { hit: true, distanceMeters: 0, surface: originState.surface ?? null };
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const boundaryDistance = channel === 'kerb'
|
|
168
|
+
? minFinite(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
|
|
169
|
+
: boundaries.kerbOuterDistance;
|
|
170
|
+
if (boundaryDistance == null) return null;
|
|
171
|
+
hits[channel] = {
|
|
172
|
+
hit: true,
|
|
173
|
+
distanceMeters: simUnitsToMeters(boundaryDistance),
|
|
174
|
+
surface: channel === 'kerb'
|
|
175
|
+
? 'kerb'
|
|
176
|
+
: surfaceBeyondKerb(track, originState.signedOffset ?? 0, lateral, metersToSimUnits(ray.lengthMeters)),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return hits;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function usesMainTrackOnlyRays(car) {
|
|
184
|
+
return car?.interaction?.profile === 'batch-training';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function hitAbsOffsetBand({ offset, lateral, minAbsOffset, maxAbsOffset, maxDistance, lengthMeters, surface }) {
|
|
188
|
+
const currentAbs = Math.abs(offset);
|
|
189
|
+
if (currentAbs >= minAbsOffset && currentAbs <= maxAbsOffset) {
|
|
190
|
+
return { hit: true, distanceMeters: 0, surface };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const candidates = [
|
|
194
|
+
distanceToOffset(minAbsOffset, offset, lateral),
|
|
195
|
+
distanceToOffset(-minAbsOffset, offset, lateral),
|
|
196
|
+
].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance);
|
|
197
|
+
|
|
198
|
+
if (Number.isFinite(maxAbsOffset)) {
|
|
199
|
+
candidates.push(
|
|
200
|
+
...[
|
|
201
|
+
distanceToOffset(maxAbsOffset, offset, lateral),
|
|
202
|
+
distanceToOffset(-maxAbsOffset, offset, lateral),
|
|
203
|
+
].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const distance = candidates
|
|
208
|
+
.filter((candidate) => {
|
|
209
|
+
const nextAbs = Math.abs(offset + lateral * candidate);
|
|
210
|
+
return nextAbs >= minAbsOffset - 1e-6 && nextAbs <= maxAbsOffset + 1e-6;
|
|
211
|
+
})
|
|
212
|
+
.sort((a, b) => a - b)[0];
|
|
213
|
+
|
|
214
|
+
if (!Number.isFinite(distance)) return createSurfaceMiss(lengthMeters);
|
|
215
|
+
return { hit: true, distanceMeters: simUnitsToMeters(distance), surface };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function distanceToOffset(targetOffset, offset, lateral) {
|
|
219
|
+
return (targetOffset - offset) / lateral;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function surfaceBeyondKerb(track, offset, lateral, maxDistance) {
|
|
223
|
+
const gravelOuter = track.width / 2 + (track.kerbWidth ?? 0) + (track.gravelWidth ?? 0);
|
|
224
|
+
const hitDistance = Math.min(
|
|
225
|
+
...[
|
|
226
|
+
distanceToOffset(track.width / 2 + (track.kerbWidth ?? 0), offset, lateral),
|
|
227
|
+
distanceToOffset(-track.width / 2 - (track.kerbWidth ?? 0), offset, lateral),
|
|
228
|
+
].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance),
|
|
229
|
+
);
|
|
230
|
+
const hitAbs = Math.abs(offset + lateral * hitDistance);
|
|
231
|
+
return hitAbs <= gravelOuter ? 'gravel' : 'grass';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isNearPitConnector(track, state) {
|
|
235
|
+
const pitLane = track.pitLane;
|
|
236
|
+
if (!pitLane?.enabled || !Number.isFinite(state?.distance)) return false;
|
|
237
|
+
const window = metersToSimUnits(PIT_CONNECTOR_RAY_FALLBACK_METERS);
|
|
238
|
+
const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
|
|
239
|
+
const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
|
|
240
|
+
return wrappedTrackDistance(state.distance, entryDistance, track.length) <= window ||
|
|
241
|
+
wrappedTrackDistance(state.distance, exitDistance, track.length) <= window;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function wrappedTrackDistance(first, second, totalLength) {
|
|
245
|
+
if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(totalLength) || totalLength <= 0) {
|
|
246
|
+
return Infinity;
|
|
247
|
+
}
|
|
248
|
+
const delta = Math.abs(first - second);
|
|
249
|
+
return Math.min(delta, totalLength - delta);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function refineSurfaceTransition(
|
|
253
|
+
track,
|
|
254
|
+
car,
|
|
255
|
+
origin,
|
|
256
|
+
ray,
|
|
257
|
+
progressHint,
|
|
258
|
+
lowDistance,
|
|
259
|
+
highDistance,
|
|
260
|
+
channel,
|
|
261
|
+
refineSteps = TRACK_RAY_REFINE_STEPS,
|
|
262
|
+
) {
|
|
263
|
+
let low = lowDistance;
|
|
264
|
+
let high = highDistance;
|
|
265
|
+
for (let index = 0; index < refineSteps; index += 1) {
|
|
266
|
+
const middle = (low + high) / 2;
|
|
267
|
+
const state = nearestRayTrackState(track, car, pointOnRay(origin, ray, middle), progressHint);
|
|
268
|
+
if (matchesSurfaceChannel(channel, state)) high = middle;
|
|
269
|
+
else low = middle;
|
|
270
|
+
}
|
|
271
|
+
return high;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function refineStepsForPrecision(precision) {
|
|
275
|
+
return precision === 'debug' ? TRACK_RAY_REFINE_STEPS : DRIVER_RAY_REFINE_STEPS;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function matchesSurfaceChannel(channel, state) {
|
|
279
|
+
if (channel === 'kerb') return state.surface === 'kerb';
|
|
280
|
+
if (channel === 'illegalSurface') return !LEGAL_SURFACES.has(state.surface) && state.surface !== 'barrier';
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function nearestRayTrackState(track, car, point, progressHint) {
|
|
285
|
+
const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
|
|
286
|
+
return nearestTrackState(track, point, progressHint, {
|
|
287
|
+
allowPitOverride: pitOverrideAllowedForCar(car),
|
|
288
|
+
indexMode: useBatchTrainingIndexMode ? 'sample' : undefined,
|
|
289
|
+
hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function minFinite(...values) {
|
|
294
|
+
const minimum = Math.min(...values.filter((value) => Number.isFinite(value)));
|
|
295
|
+
return Number.isFinite(minimum) ? minimum : null;
|
|
296
|
+
}
|