@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
|
@@ -2,6 +2,7 @@ import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
|
|
|
2
2
|
import { normalizePitCrewStats } from './pitState.js';
|
|
3
3
|
import { beginPitPenaltyService } from './pitPenaltyService.js';
|
|
4
4
|
import { beginTireService } from './pitTireService.js';
|
|
5
|
+
import { syncPitCarKinematics } from './pitKinematics.js';
|
|
5
6
|
import { getPitBoxRaceDistance } from './pitOccupancy.js';
|
|
6
7
|
|
|
7
8
|
export { normalizePitCrewStats };
|
|
@@ -32,6 +33,7 @@ export function beginPitService(sim, car, box) {
|
|
|
32
33
|
car.steeringAngle = 0;
|
|
33
34
|
car.yawRate = 0;
|
|
34
35
|
car.turnRadius = Infinity;
|
|
36
|
+
syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
|
|
35
37
|
applyWheelSurfaceState(car, sim.track);
|
|
36
38
|
car.progress = car.trackState.distance;
|
|
37
39
|
car.raceDistance = getPitBoxRaceDistance(sim, stop, box);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
pitMainLanePointAt,
|
|
7
7
|
routePoint,
|
|
8
8
|
} from './pitRouting.js';
|
|
9
|
+
import { syncPitCarKinematics } from './pitKinematics.js';
|
|
9
10
|
import { getPitBoxRaceDistance, getPitStopBox } from './pitOccupancy.js';
|
|
10
11
|
import { calculatePitServiceProfile } from './pitServiceProfile.js';
|
|
11
12
|
import { PIT_BOX_APPROACH_DISTANCE, PIT_EXIT_RELEASE_SPEED_KPH } from './pitServiceConstants.js';
|
|
@@ -79,6 +80,7 @@ export function finishPitExit(sim, car) {
|
|
|
79
80
|
car.speed = Math.max(car.speed, kphToSimSpeed(PIT_EXIT_RELEASE_SPEED_KPH));
|
|
80
81
|
car.throttle = 0.55;
|
|
81
82
|
car.brake = 0;
|
|
83
|
+
syncPitCarKinematics(car, { physicsMode: sim.physicsMode });
|
|
82
84
|
applyWheelSurfaceState(car, sim.track);
|
|
83
85
|
car.progress = car.trackState.distance;
|
|
84
86
|
car.raceDistance = Math.max(car.raceDistance ?? 0, stop.routeEndRaceDistance ?? car.raceDistance ?? 0);
|
|
@@ -2,6 +2,7 @@ import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
|
|
|
2
2
|
import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
|
|
3
3
|
import { clamp } from '../simMath.js';
|
|
4
4
|
import { nearestTrackStateForCar } from '../track/trackStatePolicy.js';
|
|
5
|
+
import { freezeVehicleMotion } from '../vehicle/vehicleKinematics.js';
|
|
5
6
|
|
|
6
7
|
export function setPitLaneOpenState(sim, open) {
|
|
7
8
|
const next = Boolean(open);
|
|
@@ -58,12 +59,7 @@ export function holdGridCars(sim) {
|
|
|
58
59
|
car.x = position.x;
|
|
59
60
|
car.y = position.y;
|
|
60
61
|
car.heading = gridPoint.heading;
|
|
61
|
-
car.
|
|
62
|
-
car.throttle = 0;
|
|
63
|
-
car.brake = 1;
|
|
64
|
-
car.steeringAngle = 0;
|
|
65
|
-
car.yawRate = 0;
|
|
66
|
-
car.turnRadius = Infinity;
|
|
62
|
+
freezeVehicleMotion(car, { physicsMode: sim.physicsMode });
|
|
67
63
|
car.progress = gridPoint.distance;
|
|
68
64
|
car.raceDistance = car.gridDistance;
|
|
69
65
|
applyWheelSurfaceState(car, sim.track);
|
|
@@ -14,13 +14,18 @@ import {
|
|
|
14
14
|
} from '../timing/raceTiming.js';
|
|
15
15
|
import { progressDelta } from './raceDistance.js';
|
|
16
16
|
import { isRaceDnf } from './retirements.js';
|
|
17
|
+
import { freezeVehicleMotion } from '../vehicle/vehicleKinematics.js';
|
|
17
18
|
|
|
18
19
|
export function recalculateRaceStateForSimulation(sim, { updateDrs = true } = {}) {
|
|
19
20
|
sim.cars.forEach((car) => {
|
|
20
21
|
const previousRaceDistance = car.raceDistance;
|
|
21
22
|
if (car.destroyed || car.outOfRace) {
|
|
22
23
|
applyWheelSurfaceState(car, sim.track);
|
|
23
|
-
car
|
|
24
|
+
freezeVehicleMotion(car, {
|
|
25
|
+
clearManualControls: true,
|
|
26
|
+
physicsMode: sim.physicsMode,
|
|
27
|
+
stabilityState: car.destroyed ? 'destroyed' : car.stabilityState,
|
|
28
|
+
});
|
|
24
29
|
car.canAttack = false;
|
|
25
30
|
car.drsEligible = false;
|
|
26
31
|
car.drsActive = false;
|
|
@@ -29,7 +29,6 @@ export function initializeRaceSimulation(simulation, {
|
|
|
29
29
|
track = null,
|
|
30
30
|
trackSeed = null,
|
|
31
31
|
trackGeneration = {},
|
|
32
|
-
trackQueryIndex = true,
|
|
33
32
|
physicsMode = 'arcade',
|
|
34
33
|
participantInteractions = {},
|
|
35
34
|
replayGhosts = [],
|
|
@@ -43,11 +42,8 @@ export function initializeRaceSimulation(simulation, {
|
|
|
43
42
|
...builtTrack,
|
|
44
43
|
pitLane: clonePitLaneModel(builtTrack.pitLane),
|
|
45
44
|
};
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const forkedCachedIndex = forkTrackQueryIndex(builtTrack.queryIndex);
|
|
49
|
-
attachTrackQueryIndex(simulation.track, forkedCachedIndex ?? createTrackQueryIndex(simulation.track));
|
|
50
|
-
}
|
|
45
|
+
const forkedCachedIndex = forkTrackQueryIndex(builtTrack.queryIndex, simulation.track);
|
|
46
|
+
attachTrackQueryIndex(simulation.track, forkedCachedIndex ?? createTrackQueryIndex(simulation.track));
|
|
51
47
|
simulation.track.timingLines = createTimingLines(simulation.track);
|
|
52
48
|
simulation.trackSeed = simulation.track.seed ?? trackSeed;
|
|
53
49
|
simulation.rules = normalizeRaceRules(rules);
|
|
@@ -40,7 +40,7 @@ export function runRaceStep(simulation, dt) {
|
|
|
40
40
|
];
|
|
41
41
|
driveCars.forEach((car) => {
|
|
42
42
|
if (car.destroyed || car.outOfRace) {
|
|
43
|
-
freezeRetiredCar(car);
|
|
43
|
+
freezeRetiredCar(car, { physicsMode: simulation.physicsMode });
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
const orderIndex = orderedIndexById.get(car.id);
|
|
@@ -59,7 +59,7 @@ export function runRaceStep(simulation, dt) {
|
|
|
59
59
|
throttle: controls.throttle ?? 0,
|
|
60
60
|
brake: controls.brake ?? 0,
|
|
61
61
|
};
|
|
62
|
-
if (simulation.physicsMode === '
|
|
62
|
+
if (simulation.physicsMode === 'advanced') {
|
|
63
63
|
applyWheelSurfaceState(car, simulation.track);
|
|
64
64
|
}
|
|
65
65
|
integrateVehiclePhysics(car, controls, delta, {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { freezeVehicleMotion } from '../vehicle/vehicleKinematics.js';
|
|
2
|
+
|
|
1
3
|
export function applyRedFlagHoldForSimulation(sim) {
|
|
2
4
|
sim.cars.forEach((car) => {
|
|
3
5
|
car.previousX = car.x;
|
|
4
6
|
car.previousY = car.y;
|
|
5
7
|
car.previousHeading = car.heading;
|
|
6
|
-
car.
|
|
7
|
-
car.throttle = 0;
|
|
8
|
-
car.brake = 1;
|
|
8
|
+
freezeVehicleMotion(car, { physicsMode: sim.physicsMode });
|
|
9
9
|
car.drsActive = false;
|
|
10
10
|
car.drsEligible = false;
|
|
11
11
|
car.drsZoneId = null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pointAt } from '../track/trackModel.js';
|
|
2
2
|
import { kphToSimSpeed } from '../units.js';
|
|
3
|
+
import { freezeVehicleMotion, syncVehicleVelocityToHeading } from '../vehicle/vehicleKinematics.js';
|
|
3
4
|
|
|
4
5
|
export function setSafetyCarState(sim, deployed) {
|
|
5
6
|
const next = Boolean(deployed);
|
|
@@ -60,9 +61,7 @@ export function setRedFlagState(sim, deployed) {
|
|
|
60
61
|
sim.raceControl.mode = 'red-flag';
|
|
61
62
|
sim.raceControl.frozenOrder = sim.orderedCars().map((car) => car.id);
|
|
62
63
|
sim.cars.forEach((car) => {
|
|
63
|
-
car.
|
|
64
|
-
car.throttle = 0;
|
|
65
|
-
car.brake = 1;
|
|
64
|
+
freezeVehicleMotion(car, { physicsMode: sim.physicsMode });
|
|
66
65
|
car.drsActive = false;
|
|
67
66
|
car.drsEligible = false;
|
|
68
67
|
car.drsZoneId = null;
|
|
@@ -75,8 +74,15 @@ export function setRedFlagState(sim, deployed) {
|
|
|
75
74
|
? sim.orderedCars().map((car) => car.id)
|
|
76
75
|
: null;
|
|
77
76
|
sim.cars.forEach((car) => {
|
|
78
|
-
if (
|
|
77
|
+
if (car.destroyed || car.outOfRace) {
|
|
78
|
+
freezeVehicleMotion(car, {
|
|
79
|
+
clearManualControls: true,
|
|
80
|
+
physicsMode: sim.physicsMode,
|
|
81
|
+
stabilityState: car.destroyed ? 'destroyed' : car.stabilityState,
|
|
82
|
+
});
|
|
83
|
+
} else if (!car.finished) {
|
|
79
84
|
car.speed = Math.max(car.speed, kphToSimSpeed(60));
|
|
85
|
+
if (sim.physicsMode === 'advanced') syncVehicleVelocityToHeading(car);
|
|
80
86
|
car.brake = 0;
|
|
81
87
|
car.throttle = Math.max(car.throttle ?? 0, 0.35);
|
|
82
88
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { markCarDnf } from './retirements.js';
|
|
2
2
|
import { simSpeedToKph } from '../units.js';
|
|
3
|
+
import { freezeVehicleMotion } from '../vehicle/vehicleKinematics.js';
|
|
3
4
|
|
|
4
5
|
const LEGAL_STALL_SURFACES = new Set(['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box']);
|
|
5
6
|
|
|
@@ -48,7 +49,7 @@ function retireStalledCar(sim, car) {
|
|
|
48
49
|
if (car.finished || car.destroyed || car.outOfRace) return;
|
|
49
50
|
car.outOfRace = true;
|
|
50
51
|
markCarDnf(sim, car, { reason: 'stalled-off-track' });
|
|
51
|
-
freezeRetiredCar(car);
|
|
52
|
+
freezeRetiredCar(car, { physicsMode: sim.physicsMode });
|
|
52
53
|
sim.events.unshift({
|
|
53
54
|
type: 'car-dnf',
|
|
54
55
|
at: sim.time,
|
|
@@ -58,27 +59,17 @@ function retireStalledCar(sim, car) {
|
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
export function freezeRetiredCar(car) {
|
|
62
|
-
car
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
car.appliedControls = { steering: 0, throttle: 0, brake: 1 };
|
|
68
|
-
car.manualControls = null;
|
|
62
|
+
export function freezeRetiredCar(car, { physicsMode = 'arcade' } = {}) {
|
|
63
|
+
freezeVehicleMotion(car, {
|
|
64
|
+
clearManualControls: true,
|
|
65
|
+
physicsMode,
|
|
66
|
+
stabilityState: 'destroyed',
|
|
67
|
+
});
|
|
69
68
|
car.canAttack = false;
|
|
70
69
|
car.drsActive = false;
|
|
71
70
|
car.drsEligible = false;
|
|
72
71
|
car.drsZoneId = null;
|
|
73
72
|
car.drsZoneEnabled = false;
|
|
74
|
-
car.steeringAngle = 0;
|
|
75
|
-
car.yawRate = 0;
|
|
76
|
-
car.longitudinalAcceleration = 0;
|
|
77
|
-
car.lateralAcceleration = 0;
|
|
78
|
-
car.longitudinalG = 0;
|
|
79
|
-
car.lateralG = 0;
|
|
80
|
-
car.tractionLimited = false;
|
|
81
|
-
car.stabilityState = 'destroyed';
|
|
82
73
|
}
|
|
83
74
|
|
|
84
75
|
function resetStallTimer(car) {
|
|
@@ -152,6 +152,7 @@ export class F1RaceSimulation {
|
|
|
152
152
|
cars: this.cars,
|
|
153
153
|
computeLap: (raceDistance) => this.computeLap(raceDistance),
|
|
154
154
|
nearestDistanceOnRoute,
|
|
155
|
+
physicsMode: this.physicsMode,
|
|
155
156
|
progressDelta,
|
|
156
157
|
raceControl: this.raceControl,
|
|
157
158
|
releaseRaceStart: () => this.releaseRaceStart(),
|
|
@@ -11,6 +11,9 @@ function forwardVector(car) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function velocityVector(car) {
|
|
14
|
+
if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
|
|
15
|
+
return { x: car.velocityX, y: car.velocityY };
|
|
16
|
+
}
|
|
14
17
|
const forward = forwardVector(car);
|
|
15
18
|
return { x: forward.x * car.speed, y: forward.y * car.speed };
|
|
16
19
|
}
|
|
@@ -33,9 +33,10 @@ function getLapTelemetryPosition(track, raceDistance, totalLaps = Infinity) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function createSectorProgress(position, currentSectors = null) {
|
|
36
|
+
function createSectorProgress(position, currentSectors = null, { preserveCompletedSectors = false } = {}) {
|
|
37
37
|
const activeIndex = clamp((position.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
|
|
38
38
|
return createEmptySectorTimes().map((_, index) => {
|
|
39
|
+
if (preserveCompletedSectors && Number.isFinite(currentSectors?.[index])) return 1;
|
|
39
40
|
if (index < activeIndex && Number.isFinite(currentSectors?.[index])) return 1;
|
|
40
41
|
if (index === activeIndex) return clamp(position.currentSectorProgress ?? 0, 0, 1);
|
|
41
42
|
return 0;
|
|
@@ -99,11 +100,23 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
|
|
|
99
100
|
const previousUpdateTime = Number.isFinite(telemetry.lastUpdatedAt) ? telemetry.lastUpdatedAt : currentTime;
|
|
100
101
|
const elapsedTime = Math.max(0, currentTime - previousUpdateTime);
|
|
101
102
|
|
|
102
|
-
if (
|
|
103
|
+
if (
|
|
104
|
+
!Number.isFinite(previousDistance) ||
|
|
105
|
+
!Number.isFinite(currentDistance) ||
|
|
106
|
+
Math.abs(travelled) > track.length / 2
|
|
107
|
+
) {
|
|
103
108
|
resetLapTelemetry(car, currentTime, track, totalLaps);
|
|
104
109
|
return;
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
if (travelled < -1e-3) {
|
|
113
|
+
syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps, {
|
|
114
|
+
preserveCompletedSectors: true,
|
|
115
|
+
});
|
|
116
|
+
telemetry.lastUpdatedAt = currentTime;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
107
120
|
const sectorLength = getSectorLength(track);
|
|
108
121
|
if (travelled > 1e-6) {
|
|
109
122
|
const firstBoundary = Math.floor(previousDistance / sectorLength) + 1;
|
|
@@ -155,28 +168,34 @@ function clearFutureSectorTelemetry(telemetry) {
|
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
function syncLiveSectorTelemetry(telemetry) {
|
|
171
|
+
function syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors = false } = {}) {
|
|
159
172
|
const activeIndex = clamp((telemetry.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
|
|
160
173
|
|
|
161
174
|
telemetry.liveSectors = createEmptySectorTimes();
|
|
162
175
|
telemetry.currentSectors.forEach((time, index) => {
|
|
163
|
-
if (index >= activeIndex) return;
|
|
176
|
+
if (!preserveCompletedSectors && index >= activeIndex) return;
|
|
164
177
|
if (!Number.isFinite(time)) return;
|
|
165
178
|
telemetry.liveSectors[index] = time;
|
|
166
179
|
});
|
|
167
|
-
telemetry.liveSectors[activeIndex]
|
|
180
|
+
if (!Number.isFinite(telemetry.liveSectors[activeIndex])) {
|
|
181
|
+
telemetry.liveSectors[activeIndex] = telemetry.currentSectorElapsed;
|
|
182
|
+
}
|
|
168
183
|
}
|
|
169
184
|
|
|
170
|
-
function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps
|
|
185
|
+
function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps, {
|
|
186
|
+
preserveCompletedSectors = false,
|
|
187
|
+
} = {}) {
|
|
171
188
|
const position = getLapTelemetryPosition(track, currentRaceDistance, totalLaps);
|
|
172
189
|
telemetry.completedLaps = position.completedLaps;
|
|
173
190
|
telemetry.currentLap = position.currentLap;
|
|
174
191
|
telemetry.currentSector = position.currentSector;
|
|
175
192
|
telemetry.currentSectorProgress = position.currentSectorProgress;
|
|
176
|
-
clearFutureSectorTelemetry(telemetry);
|
|
177
|
-
telemetry.sectorProgress = createSectorProgress(position, telemetry.currentSectors
|
|
193
|
+
if (!preserveCompletedSectors) clearFutureSectorTelemetry(telemetry);
|
|
194
|
+
telemetry.sectorProgress = createSectorProgress(position, telemetry.currentSectors, {
|
|
195
|
+
preserveCompletedSectors,
|
|
196
|
+
});
|
|
178
197
|
telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(position.completedLaps, totalLaps));
|
|
179
198
|
telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
|
|
180
199
|
telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
|
|
181
|
-
syncLiveSectorTelemetry(telemetry);
|
|
200
|
+
syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors });
|
|
182
201
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { clamp } from '../simMath.js';
|
|
2
2
|
import { metersToSimUnits } from '../units.js';
|
|
3
3
|
import { WORLD, PIT_BOX_COUNT, PIT_BOX_DEPTH, PIT_BOX_LENGTH, PIT_BOX_PAIR_GAP, PIT_BOX_TO_LANE_GAP, PIT_ENTRY_SEARCH_AFTER, PIT_ENTRY_SEARCH_BEFORE, PIT_EXIT_SEARCH_AFTER, PIT_EXIT_SEARCH_BEFORE, PIT_LANE_EDGE_GAP, PIT_LANE_ENTRY_BUFFER, PIT_LANE_EXIT_BUFFER, PIT_LANE_FINISH_RATIO, PIT_LANE_OFFSET_SEARCH_STEP, PIT_LANE_WIDTH, PIT_SERVICE_AREA_DEPTH, PIT_SERVICE_AREA_LENGTH, PIT_SERVICE_QUEUE_GAP, PIT_TEAM_COUNT, PIT_TEAM_GAP, PIT_BOXES_PER_TEAM, PIT_WORKING_LANE_GAP, PIT_WORKING_LANE_WIDTH, PIT_ACCESS_MAX_LENGTH, PIT_ACCESS_MIN_LENGTH, PIT_ACCESS_TANGENT_RATIO, PIT_TRACK_CLEARANCE_MARGIN } from './trackConstants.js';
|
|
4
|
-
import { clonePoint, distance, headingVector, interpolatePitPoint, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
|
|
4
|
+
import { clonePoint, distance, headingVector, interpolatePitPoint, nearestPointOnPolyline, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
|
|
5
5
|
import { findPitAccessConnection, createPitAccessRoadCenterline } from './pitLaneAccess.js';
|
|
6
|
-
import {
|
|
6
|
+
import { pointAt } from './spatialQueries.js';
|
|
7
7
|
|
|
8
8
|
export function createStartStraightBasis(track) {
|
|
9
9
|
const finish = pointAt(track, 0);
|
|
@@ -90,7 +90,7 @@ export function scorePitLanePlacement(track, side, laneOffset, layout) {
|
|
|
90
90
|
minimumClearance = Math.min(minimumClearance, pointWorldClearance(lanePoint), pointWorldClearance(boxPoint));
|
|
91
91
|
minimumTrackClearance = Math.min(
|
|
92
92
|
minimumTrackClearance,
|
|
93
|
-
|
|
93
|
+
nearestMainTrackProjection(track, lanePoint).crossTrackError -
|
|
94
94
|
(track.width / 2 + (track.kerbWidth ?? 0) + PIT_TRACK_CLEARANCE_MARGIN),
|
|
95
95
|
);
|
|
96
96
|
}
|
|
@@ -105,6 +105,10 @@ export function scorePitLanePlacement(track, side, laneOffset, layout) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function nearestMainTrackProjection(track, position) {
|
|
109
|
+
return nearestPointOnPolyline(track.samples, position) ?? { crossTrackError: Infinity };
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
export function choosePitLanePlacement(track, baseLaneOffset, layout) {
|
|
109
113
|
const candidates = [];
|
|
110
114
|
for (let offsetStep = 0; offsetStep <= 9; offsetStep += 1) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clamp, normalizeAngle, wrapDistance } from '../simMath.js';
|
|
2
|
-
import {
|
|
2
|
+
import { PIT_LANE_WIDTH } from './trackConstants.js';
|
|
3
3
|
import { nearestPitLaneState } from './pitLaneState.js';
|
|
4
4
|
import { queryNearestTrackProjection } from './trackQueryIndex.js';
|
|
5
5
|
|
|
@@ -91,13 +91,10 @@ export function createTrackState(track, position, best) {
|
|
|
91
91
|
|
|
92
92
|
export function nearestTrackState(track, position, progressHint = null, options = {}) {
|
|
93
93
|
const allowPitOverride = options.allowPitOverride !== false;
|
|
94
|
-
const best =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
hintMaxDistance: options.hintMaxDistance,
|
|
99
|
-
}) ??
|
|
100
|
-
nearestTrackSampleLegacy(track, position, progressHint);
|
|
94
|
+
const best = queryNearestTrackProjection(track, position, progressHint);
|
|
95
|
+
if (!best) {
|
|
96
|
+
throw new Error('nearestTrackState requires a finite position and an indexed track model. Build tracks with buildTrackModel() or createRaceSimulation().');
|
|
97
|
+
}
|
|
101
98
|
const trackState = createTrackState(track, position, best);
|
|
102
99
|
if (!allowPitOverride) return trackState;
|
|
103
100
|
const pitState = nearestPitLaneState(track, position);
|
|
@@ -125,26 +122,6 @@ export function nearestTrackState(track, position, progressHint = null, options
|
|
|
125
122
|
};
|
|
126
123
|
}
|
|
127
124
|
|
|
128
|
-
function nearestTrackSampleLegacy(track, position, progressHint = null) {
|
|
129
|
-
let nearest = null;
|
|
130
|
-
|
|
131
|
-
if (Number.isFinite(progressHint)) {
|
|
132
|
-
const centerIndex = sampleIndexAtDistance(track, progressHint);
|
|
133
|
-
nearest = nearestSampleInRange(
|
|
134
|
-
track,
|
|
135
|
-
position,
|
|
136
|
-
centerIndex - NEAREST_HINT_WINDOW_SAMPLES,
|
|
137
|
-
centerIndex + NEAREST_HINT_WINDOW_SAMPLES,
|
|
138
|
-
);
|
|
139
|
-
const fallbackDistance = track.width / 2 + (track.kerbWidth ?? 0) + track.gravelWidth + track.runoffWidth + 180;
|
|
140
|
-
if (!nearest.best || nearest.bestDistance > fallbackDistance * fallbackDistance) {
|
|
141
|
-
nearest = null;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return nearest?.best ?? nearestSampleGlobal(track, position).best;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
125
|
export function offsetTrackPoint(point, offset) {
|
|
149
126
|
return {
|
|
150
127
|
x: point.x + point.normalX * offset,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { clamp } from '../simMath.js';
|
|
2
|
+
import { ensureQueryScratch, ensureScratchArray, nextScratchEpoch } from './trackQueryScratch.js';
|
|
3
|
+
|
|
4
|
+
export function createSpatialGrid(bounds, cellSize) {
|
|
5
|
+
const columns = Math.max(1, Math.ceil((bounds.maxX - bounds.minX) / cellSize));
|
|
6
|
+
const rows = Math.max(1, Math.ceil((bounds.maxY - bounds.minY) / cellSize));
|
|
7
|
+
return {
|
|
8
|
+
bounds,
|
|
9
|
+
cellSize,
|
|
10
|
+
columns,
|
|
11
|
+
rows,
|
|
12
|
+
cells: new Array(columns * rows),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function insertIdIntoGridBounds(grid, id, bounds) {
|
|
17
|
+
const minCell = gridCellForPoint(grid, { x: bounds.minX, y: bounds.minY }, true);
|
|
18
|
+
const maxCell = gridCellForPoint(grid, { x: bounds.maxX, y: bounds.maxY }, true);
|
|
19
|
+
for (let row = minCell.row; row <= maxCell.row; row += 1) {
|
|
20
|
+
for (let column = minCell.column; column <= maxCell.column; column += 1) {
|
|
21
|
+
const cellIndex = row * grid.columns + column;
|
|
22
|
+
const cell = grid.cells[cellIndex];
|
|
23
|
+
if (cell) cell.push(id);
|
|
24
|
+
else grid.cells[cellIndex] = [id];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function candidateIdsFromGrid(index, grid, position, neighborLimit) {
|
|
30
|
+
const center = gridCellForPoint(grid, position, false);
|
|
31
|
+
if (!center) return [];
|
|
32
|
+
const scratch = ensureQueryScratch(index);
|
|
33
|
+
const maxId = Math.max(index.centerline.segmentCount, index.pit?.roadSegments?.length ?? 0, index.pit?.boxCandidates?.length ?? 0);
|
|
34
|
+
const idMarks = ensureScratchArray(scratch, 'candidateMarks', maxId);
|
|
35
|
+
const ids = [];
|
|
36
|
+
for (let radius = 0; radius <= neighborLimit; radius += 1) {
|
|
37
|
+
const candidateEpoch = nextScratchEpoch(scratch, 'candidateEpoch', idMarks);
|
|
38
|
+
ids.length = 0;
|
|
39
|
+
for (let row = center.row - radius; row <= center.row + radius; row += 1) {
|
|
40
|
+
for (let column = center.column - radius; column <= center.column + radius; column += 1) {
|
|
41
|
+
if (column < 0 || row < 0 || column >= grid.columns || row >= grid.rows) continue;
|
|
42
|
+
const cell = grid.cells[row * grid.columns + column];
|
|
43
|
+
if (!cell) continue;
|
|
44
|
+
for (const id of cell) {
|
|
45
|
+
if (idMarks[id] === candidateEpoch) continue;
|
|
46
|
+
idMarks[id] = candidateEpoch;
|
|
47
|
+
ids.push(id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (ids.length) return ids.slice();
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function candidateIdsFromGridBounds(index, grid, bounds) {
|
|
57
|
+
if (
|
|
58
|
+
bounds.maxX < grid.bounds.minX ||
|
|
59
|
+
bounds.minX > grid.bounds.maxX ||
|
|
60
|
+
bounds.maxY < grid.bounds.minY ||
|
|
61
|
+
bounds.minY > grid.bounds.maxY
|
|
62
|
+
) return [];
|
|
63
|
+
const minPoint = { x: bounds.minX, y: bounds.minY };
|
|
64
|
+
const maxPoint = { x: bounds.maxX, y: bounds.maxY };
|
|
65
|
+
const minCell = gridCellForPoint(grid, minPoint, true);
|
|
66
|
+
const maxCell = gridCellForPoint(grid, maxPoint, true);
|
|
67
|
+
const scratch = ensureQueryScratch(index);
|
|
68
|
+
const maxId = Math.max(index.centerline.segmentCount, index.pit?.roadSegments?.length ?? 0, index.pit?.boxCandidates?.length ?? 0);
|
|
69
|
+
const idMarks = ensureScratchArray(scratch, 'candidateMarks', maxId);
|
|
70
|
+
const candidateEpoch = nextScratchEpoch(scratch, 'candidateEpoch', idMarks);
|
|
71
|
+
const ids = [];
|
|
72
|
+
for (let row = minCell.row; row <= maxCell.row; row += 1) {
|
|
73
|
+
for (let column = minCell.column; column <= maxCell.column; column += 1) {
|
|
74
|
+
const cell = grid.cells[row * grid.columns + column];
|
|
75
|
+
if (!cell) continue;
|
|
76
|
+
for (const id of cell) {
|
|
77
|
+
if (idMarks[id] === candidateEpoch) continue;
|
|
78
|
+
idMarks[id] = candidateEpoch;
|
|
79
|
+
ids.push(id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return ids;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function gridCellForPoint(grid, point, clampToGrid) {
|
|
87
|
+
const column = Math.floor((point.x - grid.bounds.minX) / grid.cellSize);
|
|
88
|
+
const row = Math.floor((point.y - grid.bounds.minY) / grid.cellSize);
|
|
89
|
+
if (!clampToGrid && (column < 0 || row < 0 || column >= grid.columns || row >= grid.rows)) return null;
|
|
90
|
+
return {
|
|
91
|
+
column: clampToGrid ? clamp(column, 0, grid.columns - 1) : column,
|
|
92
|
+
row: clampToGrid ? clamp(row, 0, grid.rows - 1) : row,
|
|
93
|
+
};
|
|
94
|
+
}
|