@inventure71/paddockjs 4.1.0 → 4.2.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 +42 -0
- package/INSTALL_AND_UPDATE.md +9 -2
- package/README.md +4 -2
- package/docs/architecture.md +2 -0
- package/docs/component_inventory.md +1 -1
- package/docs/getting-started.md +3 -0
- package/docs/index.md +4 -2
- package/docs/loading.md +141 -0
- package/docs/system_specs.md +4 -1
- package/docs/testing-reliability-guide.md +62 -0
- package/package.json +5 -7
- package/src/data/index.d.ts +64 -1
- package/src/environment/index.d.ts +75 -12
- package/src/index.d.ts +72 -6
- package/src/simulation/collisionGeometry.js +7 -9
- package/src/simulation/driver/driverController.js +2 -2
- package/src/simulation/driver/edgeRecovery.js +2 -2
- package/src/simulation/driver/racingControls.js +35 -25
- package/src/simulation/driver/racingLinePlan.js +6 -5
- package/src/simulation/driver/rejoinControls.js +3 -3
- package/src/simulation/driver/trafficScan.js +48 -22
- package/src/simulation/participants/participantInteractions.js +24 -15
- package/src/simulation/race/raceStep.js +19 -10
- package/src/simulation/race/safetyCar.js +2 -1
- package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
- package/src/simulation/rules/rulesReview.js +19 -9
- package/src/simulation/rules/trackLimitsSteward.js +2 -2
- package/src/simulation/timing/sectorPerformance.js +11 -3
- package/src/simulation/track/spatialQueries.js +80 -11
- package/src/simulation/track/trackModel.js +10 -1
- package/src/simulation/vehicle/contactResolution.js +4 -4
- package/src/simulation/vehicle/vehicleGeometry.js +87 -49
- package/src/simulation/vehicle/vehiclePhysics.js +7 -2
- package/src/simulation/vehicle/vehicleSnapshots.js +5 -2
- package/src/simulation/vehicle/vehicleState.js +3 -2
- package/src/ui/raceCanvasTemplate.js +3 -2
- package/examples/train-basic-policy.mjs +0 -276
- package/examples/trainingData.mjs +0 -133
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decideDriverControls } from '../driverController.js';
|
|
2
2
|
import { clamp } from '../simMath.js';
|
|
3
3
|
import { updateReplayGhosts } from '../replay/replayGhosts.js';
|
|
4
|
-
import { integrateVehiclePhysics } from '../vehicle/vehiclePhysics.js';
|
|
4
|
+
import { integrateVehiclePhysics, isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
5
5
|
import { applyWheelSurfaceState } from '../vehicle/wheelSurface.js';
|
|
6
6
|
import { applyRedFlagHoldForSimulation } from './redFlag.js';
|
|
7
7
|
import { freezeRetiredCar, updateStalledDnfForSimulation } from './stalledDnf.js';
|
|
@@ -38,19 +38,28 @@ export function runRaceStep(simulation, dt) {
|
|
|
38
38
|
|
|
39
39
|
const orderedCars = simulation.orderedCars();
|
|
40
40
|
const raceContext = simulation.driverRaceContext(orderedCars);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
for (let index = 0; index < simulation.cars.length; index += 1) {
|
|
42
|
+
simulation.cars[index]._driveOrderIndex = -1;
|
|
43
|
+
}
|
|
44
|
+
for (let index = 0; index < orderedCars.length; index += 1) {
|
|
45
|
+
orderedCars[index]._driveOrderIndex = index;
|
|
46
|
+
}
|
|
47
|
+
let driveCars = orderedCars;
|
|
48
|
+
if (orderedCars.length !== simulation.cars.length) {
|
|
49
|
+
driveCars = simulation._driveCarsScratch ??= [];
|
|
50
|
+
driveCars.length = 0;
|
|
51
|
+
driveCars.push(...orderedCars);
|
|
52
|
+
for (let index = 0; index < simulation.cars.length; index += 1) {
|
|
53
|
+
const car = simulation.cars[index];
|
|
54
|
+
if (car._driveOrderIndex === -1) driveCars.push(car);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
48
57
|
driveCars.forEach((car) => {
|
|
49
58
|
if (car.destroyed || car.outOfRace) {
|
|
50
59
|
freezeRetiredCar(car, { physicsMode: simulation.physicsMode });
|
|
51
60
|
return;
|
|
52
61
|
}
|
|
53
|
-
const orderIndex =
|
|
62
|
+
const orderIndex = car._driveOrderIndex >= 0 ? car._driveOrderIndex : undefined;
|
|
54
63
|
car.previousX = car.x;
|
|
55
64
|
car.previousY = car.y;
|
|
56
65
|
car.previousHeading = car.heading;
|
|
@@ -66,7 +75,7 @@ export function runRaceStep(simulation, dt) {
|
|
|
66
75
|
throttle: controls.throttle ?? 0,
|
|
67
76
|
brake: controls.brake ?? 0,
|
|
68
77
|
};
|
|
69
|
-
if (simulation.physicsMode
|
|
78
|
+
if (isSimulatorPhysicsMode(simulation.physicsMode)) {
|
|
70
79
|
measureRuntimePhase(simulation, 'prePhysicsWheelSurface', () => {
|
|
71
80
|
applyWheelSurfaceState(car, simulation.track);
|
|
72
81
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pointAt } from '../track/trackModel.js';
|
|
2
2
|
import { kphToSimSpeed } from '../units.js';
|
|
3
3
|
import { freezeVehicleMotion, syncVehicleVelocityToHeading } from '../vehicle/vehicleKinematics.js';
|
|
4
|
+
import { isSimulatorPhysicsMode } from '../vehicle/vehiclePhysics.js';
|
|
4
5
|
|
|
5
6
|
export function setSafetyCarState(sim, deployed) {
|
|
6
7
|
const next = Boolean(deployed);
|
|
@@ -82,7 +83,7 @@ export function setRedFlagState(sim, deployed) {
|
|
|
82
83
|
});
|
|
83
84
|
} else if (!car.finished) {
|
|
84
85
|
car.speed = Math.max(car.speed, kphToSimSpeed(60));
|
|
85
|
-
if (sim.physicsMode
|
|
86
|
+
if (isSimulatorPhysicsMode(sim.physicsMode)) syncVehicleVelocityToHeading(car);
|
|
86
87
|
car.brake = 0;
|
|
87
88
|
car.throttle = Math.max(car.throttle ?? 0, 0.35);
|
|
88
89
|
}
|
|
@@ -20,7 +20,7 @@ export function calculatePitLaneSpeedingReview({ car, rule, stewardState }) {
|
|
|
20
20
|
|
|
21
21
|
if (!isViolation) {
|
|
22
22
|
return {
|
|
23
|
-
nextState: { ...current, active: false },
|
|
23
|
+
nextState: current.active === false ? current : { ...current, active: false },
|
|
24
24
|
event: null,
|
|
25
25
|
penalty: null,
|
|
26
26
|
};
|
|
@@ -129,7 +129,9 @@ export function reviewTrackLimitsForSimulation(sim) {
|
|
|
129
129
|
sim.cars.forEach((car) => {
|
|
130
130
|
if (isLegallyInsidePitLaneForTrackLimits(car)) {
|
|
131
131
|
const current = sim.stewardState.trackLimits[car.id];
|
|
132
|
-
|
|
132
|
+
if (!current || current.active !== false) {
|
|
133
|
+
sim.stewardState.trackLimits[car.id] = { ...(current ?? { violations: 0 }), active: false };
|
|
134
|
+
}
|
|
133
135
|
return;
|
|
134
136
|
}
|
|
135
137
|
|
|
@@ -146,18 +148,26 @@ export function reviewTrackLimitsForSimulation(sim) {
|
|
|
146
148
|
});
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
const PIT_SPEED_REVIEW_SCRATCH = {
|
|
152
|
+
car: { id: null, speedKph: 0, trackState: null, pitLanePart: null },
|
|
153
|
+
rule: null,
|
|
154
|
+
stewardState: null,
|
|
155
|
+
};
|
|
156
|
+
|
|
149
157
|
export function reviewPitLaneSpeedingForSimulation(sim) {
|
|
150
158
|
const rule = getPenaltyRule(sim.rules, 'pitLaneSpeeding');
|
|
151
159
|
sim.cars.forEach((car) => {
|
|
152
160
|
const currentState = sim.stewardState.pitLaneSpeeding[car.id];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
// The steward only reads the speed and pit-lane location, so feed it a
|
|
162
|
+
// pooled view instead of spreading the whole car every step.
|
|
163
|
+
const reviewCar = PIT_SPEED_REVIEW_SCRATCH.car;
|
|
164
|
+
reviewCar.id = car.id;
|
|
165
|
+
reviewCar.speedKph = simSpeedToKph(car.speed);
|
|
166
|
+
reviewCar.trackState = car.trackState;
|
|
167
|
+
reviewCar.pitLanePart = car.pitLanePart;
|
|
168
|
+
PIT_SPEED_REVIEW_SCRATCH.rule = rule;
|
|
169
|
+
PIT_SPEED_REVIEW_SCRATCH.stewardState = currentState;
|
|
170
|
+
const review = calculatePitLaneSpeedingReview(PIT_SPEED_REVIEW_SCRATCH);
|
|
161
171
|
sim.stewardState.pitLaneSpeeding[car.id] = review.nextState;
|
|
162
172
|
if (review.event) sim.events.unshift({ ...review.event, at: sim.time });
|
|
163
173
|
if (review.penalty) sim.recordPenalty(review.penalty);
|
|
@@ -22,7 +22,7 @@ export function calculateTrackLimitReview({ car, rule, track, stewardState }) {
|
|
|
22
22
|
const wheelStates = wheelSurfaceState.wheels;
|
|
23
23
|
if (wheelStates.some((wheelState) => wheelState.inPitLane)) {
|
|
24
24
|
return {
|
|
25
|
-
nextState: { ...current, active: false },
|
|
25
|
+
nextState: current.active === false ? current : { ...current, active: false },
|
|
26
26
|
event: null,
|
|
27
27
|
penalty: null,
|
|
28
28
|
};
|
|
@@ -34,7 +34,7 @@ export function calculateTrackLimitReview({ car, rule, track, stewardState }) {
|
|
|
34
34
|
|
|
35
35
|
if (!isViolation) {
|
|
36
36
|
return {
|
|
37
|
-
nextState: { ...current, active: false },
|
|
37
|
+
nextState: current.active === false ? current : { ...current, active: false },
|
|
38
38
|
event: null,
|
|
39
39
|
penalty: null,
|
|
40
40
|
};
|
|
@@ -5,7 +5,7 @@ const OVERALL_BEST_1 = '_sectorPerformanceOverallBest1';
|
|
|
5
5
|
const OVERALL_BEST_2 = '_sectorPerformanceOverallBest2';
|
|
6
6
|
|
|
7
7
|
export function createEmptySectorTimes() {
|
|
8
|
-
return Array
|
|
8
|
+
return new Array(TELEMETRY_SECTOR_COUNT).fill(null);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function createEmptySectorPerformance() {
|
|
@@ -17,11 +17,19 @@ export function createEmptySectorPerformance() {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function serializeSectorTimes(values) {
|
|
20
|
-
|
|
20
|
+
const result = new Array(TELEMETRY_SECTOR_COUNT);
|
|
21
|
+
for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
22
|
+
result[index] = finiteOrNull(values?.[index]);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export function serializeSectorPerformance(values) {
|
|
24
|
-
|
|
28
|
+
const result = new Array(TELEMETRY_SECTOR_COUNT);
|
|
29
|
+
for (let index = 0; index < TELEMETRY_SECTOR_COUNT; index += 1) {
|
|
30
|
+
result[index] = values?.[index] ?? null;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export function updateSectorPerformance(cars, stats = null) {
|
|
@@ -6,27 +6,59 @@ import { queryNearestTrackProjection, queryNearestTrackProjectionInto } from './
|
|
|
6
6
|
import { recordStat } from './trackQueryStats.js';
|
|
7
7
|
|
|
8
8
|
export function pointAt(track, distanceAlong) {
|
|
9
|
+
return pointAtInto(track, distanceAlong, {});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function pointAtInto(track, distanceAlong, target) {
|
|
9
13
|
const wrapped = wrapDistance(distanceAlong, track.length);
|
|
10
|
-
const low =
|
|
14
|
+
const low = sampleIndexAtDistanceUnwrapped(track, wrapped);
|
|
11
15
|
|
|
12
16
|
const next = track.samples[low] ?? track.samples[0];
|
|
13
17
|
const previous = track.samples[Math.max(0, low - 1)] ?? next;
|
|
14
18
|
const span = Math.max(1, next.distance - previous.distance);
|
|
15
19
|
const amount = clamp((wrapped - previous.distance) / span, 0, 1);
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
21
|
+
target.x = previous.x + (next.x - previous.x) * amount;
|
|
22
|
+
target.y = previous.y + (next.y - previous.y) * amount;
|
|
23
|
+
target.heading = previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
|
|
24
|
+
target.normalX = previous.normalX + (next.normalX - previous.normalX) * amount;
|
|
25
|
+
target.normalY = previous.normalY + (next.normalY - previous.normalY) * amount;
|
|
26
|
+
target.curvature = previous.curvature + (next.curvature - previous.curvature) * amount;
|
|
27
|
+
target.distance = wrapped;
|
|
28
|
+
return target;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
export function
|
|
31
|
+
export function sampleHeadingCurvatureAtInto(track, distanceAlong, target) {
|
|
29
32
|
const wrapped = wrapDistance(distanceAlong, track.length);
|
|
33
|
+
const low = sampleIndexAtDistanceUnwrapped(track, wrapped);
|
|
34
|
+
|
|
35
|
+
const next = track.samples[low] ?? track.samples[0];
|
|
36
|
+
const previous = track.samples[Math.max(0, low - 1)] ?? next;
|
|
37
|
+
const span = Math.max(1, next.distance - previous.distance);
|
|
38
|
+
const amount = clamp((wrapped - previous.distance) / span, 0, 1);
|
|
39
|
+
|
|
40
|
+
target.heading = previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
|
|
41
|
+
target.curvature = previous.curvature + (next.curvature - previous.curvature) * amount;
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sampleHeadingAt(track, distanceAlong) {
|
|
46
|
+
const wrapped = wrapDistance(distanceAlong, track.length);
|
|
47
|
+
const low = sampleIndexAtDistanceUnwrapped(track, wrapped);
|
|
48
|
+
|
|
49
|
+
const next = track.samples[low] ?? track.samples[0];
|
|
50
|
+
const previous = track.samples[Math.max(0, low - 1)] ?? next;
|
|
51
|
+
const span = Math.max(1, next.distance - previous.distance);
|
|
52
|
+
const amount = clamp((wrapped - previous.distance) / span, 0, 1);
|
|
53
|
+
|
|
54
|
+
return previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sampleIndexAtDistance(track, distanceAlong) {
|
|
58
|
+
return binarySampleIndexAtDistance(track, wrapDistance(distanceAlong, track.length));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function binarySampleIndexAtDistance(track, wrapped) {
|
|
30
62
|
let low = 0;
|
|
31
63
|
let high = track.samples.length - 1;
|
|
32
64
|
|
|
@@ -39,6 +71,43 @@ export function sampleIndexAtDistance(track, distanceAlong) {
|
|
|
39
71
|
return low;
|
|
40
72
|
}
|
|
41
73
|
|
|
74
|
+
// Uniform-bin index over sample distances: table[bin] holds the smallest
|
|
75
|
+
// sample index whose distance reaches the bin start, so a lookup plus a short
|
|
76
|
+
// forward walk returns exactly what the binary search would, in O(1). Built
|
|
77
|
+
// eagerly when the track query index is attached; lookups on sample arrays
|
|
78
|
+
// without a prepared table fall back to the binary search.
|
|
79
|
+
const SAMPLE_INDEX_LUTS = new WeakMap();
|
|
80
|
+
|
|
81
|
+
export function prepareSampleIndexLut(track) {
|
|
82
|
+
const samples = track?.samples;
|
|
83
|
+
if (!Array.isArray(samples) || samples.length < 2 || !Number.isFinite(track.length) || track.length <= 0) return;
|
|
84
|
+
if (SAMPLE_INDEX_LUTS.has(samples)) return;
|
|
85
|
+
const lastIndex = samples.length - 1;
|
|
86
|
+
const bins = Math.max(1, lastIndex * 2);
|
|
87
|
+
const table = new Int32Array(bins);
|
|
88
|
+
const binSize = track.length / bins;
|
|
89
|
+
let index = 0;
|
|
90
|
+
for (let bin = 0; bin < bins; bin += 1) {
|
|
91
|
+
const start = bin * binSize;
|
|
92
|
+
while (index < lastIndex && samples[index].distance < start) index += 1;
|
|
93
|
+
table[bin] = index;
|
|
94
|
+
}
|
|
95
|
+
SAMPLE_INDEX_LUTS.set(samples, { bins, binSize, table });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sampleIndexAtDistanceUnwrapped(track, wrapped) {
|
|
99
|
+
const samples = track.samples;
|
|
100
|
+
const lut = Number.isFinite(wrapped) ? SAMPLE_INDEX_LUTS.get(samples) : null;
|
|
101
|
+
if (!lut) return binarySampleIndexAtDistance(track, wrapped);
|
|
102
|
+
const lastIndex = samples.length - 1;
|
|
103
|
+
let bin = Math.floor(wrapped / lut.binSize);
|
|
104
|
+
if (bin < 0) bin = 0;
|
|
105
|
+
else if (bin >= lut.bins) bin = lut.bins - 1;
|
|
106
|
+
let index = lut.table[bin];
|
|
107
|
+
while (index < lastIndex && samples[index].distance < wrapped) index += 1;
|
|
108
|
+
return index;
|
|
109
|
+
}
|
|
110
|
+
|
|
42
111
|
export function nearestSampleInRange(track, position, startIndex, endIndex) {
|
|
43
112
|
const sampleCount = track.samples.length - 1;
|
|
44
113
|
let best = null;
|
|
@@ -6,6 +6,7 @@ import { generateCenterlineControls, generateFallbackCenterlineControls, generat
|
|
|
6
6
|
import { isValidProceduralTrackModel } from './trackValidation.js';
|
|
7
7
|
import { recalculateSampleGeometry } from './sampleGeometry.js';
|
|
8
8
|
import { rotateSamplesToStandingStart, straightenStandingStartSamples } from './startStraight.js';
|
|
9
|
+
import { prepareSampleIndexLut } from './spatialQueries.js';
|
|
9
10
|
import { createTrackSectors } from './trackSectors.js';
|
|
10
11
|
import { deriveDrsZones, normalizeDrsZone } from './drsZones.js';
|
|
11
12
|
import { createPitLaneModel } from './pitLaneLayout.js';
|
|
@@ -15,7 +16,14 @@ import { attachTrackSnapshotJsonSerializer } from './trackSnapshotJson.js';
|
|
|
15
16
|
|
|
16
17
|
export { WORLD, TRACK } from './trackConstants.js';
|
|
17
18
|
export { isInDrsZone } from './drsZones.js';
|
|
18
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
nearestTrackState,
|
|
21
|
+
offsetTrackPoint,
|
|
22
|
+
pointAt,
|
|
23
|
+
pointAtInto,
|
|
24
|
+
sampleHeadingAt,
|
|
25
|
+
sampleHeadingCurvatureAtInto,
|
|
26
|
+
} from './spatialQueries.js';
|
|
19
27
|
|
|
20
28
|
const PROCEDURAL_TRACK_CACHE = new Map();
|
|
21
29
|
const TRACK_MODEL_CACHE = new WeakMap();
|
|
@@ -57,6 +65,7 @@ export function buildTrackModel(track = TRACK) {
|
|
|
57
65
|
? null
|
|
58
66
|
: createPitLaneModel(model);
|
|
59
67
|
attachTrackQueryIndex(model, createTrackQueryIndex(model));
|
|
68
|
+
prepareSampleIndexLut(model);
|
|
60
69
|
attachTrackSnapshotJsonSerializer(model);
|
|
61
70
|
if (canReuseCachedModel) {
|
|
62
71
|
TRACK_MODEL_CACHE.set(track, freezeTrackModel(model));
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from '../collisionGeometry.js';
|
|
7
7
|
import { canCollide, isCollidable } from '../participants/participantInteractions.js';
|
|
8
8
|
import { clamp, normalizeAngle } from '../simMath.js';
|
|
9
|
-
import { VEHICLE_LIMITS } from './vehiclePhysics.js';
|
|
9
|
+
import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from './vehiclePhysics.js';
|
|
10
10
|
import { shiftPreviousRenderPose } from '../pit/pitRouting.js';
|
|
11
11
|
|
|
12
12
|
const MAX_COLLISION_CORRECTION = 4.5;
|
|
@@ -114,8 +114,8 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
if (oneCarFixed) {
|
|
117
|
-
if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode
|
|
118
|
-
if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode
|
|
117
|
+
if (!firstPitControlled) dampPitContactVelocity(first, 0.985, isSimulatorPhysicsMode(sim.physicsMode));
|
|
118
|
+
if (!secondPitControlled) dampPitContactVelocity(second, 0.985, isSimulatorPhysicsMode(sim.physicsMode));
|
|
119
119
|
} else {
|
|
120
120
|
applyContactVelocityResponse(sim, first, second, collision.axis);
|
|
121
121
|
}
|
|
@@ -167,7 +167,7 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
167
167
|
export function applyContactVelocityResponse(sim, first, second, axis, options = {}) {
|
|
168
168
|
recordContactVelocityResponse(options.stats);
|
|
169
169
|
if (
|
|
170
|
-
sim.physicsMode
|
|
170
|
+
isSimulatorPhysicsMode(sim.physicsMode) ||
|
|
171
171
|
Number.isFinite(first.velocityX) ||
|
|
172
172
|
Number.isFinite(second.velocityX)
|
|
173
173
|
) {
|
|
@@ -274,33 +274,22 @@ function sweptPoseSignature(pose) {
|
|
|
274
274
|
].join(':');
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
function geometryPose(car) {
|
|
278
|
-
const { x, y, heading } = currentPoseState(car);
|
|
279
|
-
return {
|
|
280
|
-
x,
|
|
281
|
-
y,
|
|
282
|
-
heading,
|
|
283
|
-
previousX: finiteOr(car.previousX, x),
|
|
284
|
-
previousY: finiteOr(car.previousY, y),
|
|
285
|
-
previousHeading: normalizeAngle(finiteOr(car.previousHeading, heading)),
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
277
|
function currentGeometryStateMatches(car, state) {
|
|
290
278
|
const pose = state?.pose;
|
|
291
279
|
if (!pose) return false;
|
|
292
|
-
const currentPose = currentPoseState(car);
|
|
293
280
|
return (
|
|
294
|
-
pose.x ===
|
|
295
|
-
pose.y ===
|
|
296
|
-
pose.heading ===
|
|
281
|
+
pose.x === finiteOr(car.x, 0) &&
|
|
282
|
+
pose.y === finiteOr(car.y, 0) &&
|
|
283
|
+
pose.heading === normalizeAngle(finiteOr(car.heading, 0))
|
|
297
284
|
);
|
|
298
285
|
}
|
|
299
286
|
|
|
300
287
|
function geometryStateMatches(car, state) {
|
|
301
288
|
const pose = state?.pose;
|
|
302
289
|
if (!pose) return false;
|
|
303
|
-
const
|
|
290
|
+
const x = finiteOr(car.x, 0);
|
|
291
|
+
const y = finiteOr(car.y, 0);
|
|
292
|
+
const heading = normalizeAngle(finiteOr(car.heading, 0));
|
|
304
293
|
return (
|
|
305
294
|
pose.x === x &&
|
|
306
295
|
pose.y === y &&
|
|
@@ -312,6 +301,10 @@ function geometryStateMatches(car, state) {
|
|
|
312
301
|
}
|
|
313
302
|
|
|
314
303
|
export function createVehicleShapeAabb(shape) {
|
|
304
|
+
return writeVehicleShapeAabb({}, shape);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function writeVehicleShapeAabb(target, shape) {
|
|
315
308
|
let minX = Infinity;
|
|
316
309
|
let maxX = -Infinity;
|
|
317
310
|
let minY = Infinity;
|
|
@@ -323,12 +316,11 @@ export function createVehicleShapeAabb(shape) {
|
|
|
323
316
|
if (corner.y < minY) minY = corner.y;
|
|
324
317
|
if (corner.y > maxY) maxY = corner.y;
|
|
325
318
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
};
|
|
319
|
+
target.minX = minX;
|
|
320
|
+
target.maxX = maxX;
|
|
321
|
+
target.minY = minY;
|
|
322
|
+
target.maxY = maxY;
|
|
323
|
+
return target;
|
|
332
324
|
}
|
|
333
325
|
|
|
334
326
|
export function mergeAabbs(aabbs) {
|
|
@@ -346,32 +338,73 @@ export function mergeAabbs(aabbs) {
|
|
|
346
338
|
return { minX, maxX, minY, maxY };
|
|
347
339
|
}
|
|
348
340
|
|
|
349
|
-
|
|
350
|
-
const pose =
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
341
|
+
function writeGeometryPose(target, car) {
|
|
342
|
+
const pose = target ?? {};
|
|
343
|
+
const x = finiteOr(car.x, 0);
|
|
344
|
+
const y = finiteOr(car.y, 0);
|
|
345
|
+
const heading = normalizeAngle(finiteOr(car.heading, 0));
|
|
346
|
+
pose.x = x;
|
|
347
|
+
pose.y = y;
|
|
348
|
+
pose.heading = heading;
|
|
349
|
+
pose.previousX = finiteOr(car.previousX, x);
|
|
350
|
+
pose.previousY = finiteOr(car.previousY, y);
|
|
351
|
+
pose.previousHeading = normalizeAngle(finiteOr(car.previousHeading, heading));
|
|
352
|
+
return pose;
|
|
353
|
+
}
|
|
360
354
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
previousBodyAabb,
|
|
373
|
-
sweptBodyAabb,
|
|
355
|
+
function createGeometryStateShell() {
|
|
356
|
+
const state = {
|
|
357
|
+
pose: null,
|
|
358
|
+
current: null,
|
|
359
|
+
previous: null,
|
|
360
|
+
body: null,
|
|
361
|
+
wheels: null,
|
|
362
|
+
contactPatches: null,
|
|
363
|
+
bodyAabb: {},
|
|
364
|
+
previousBodyAabb: {},
|
|
365
|
+
sweptBodyAabb: {},
|
|
374
366
|
};
|
|
367
|
+
// Signatures are derived lazily from the pose: nothing on the hot path reads
|
|
368
|
+
// them, so the per-step string formatting cost must not be paid eagerly.
|
|
369
|
+
Object.defineProperties(state, {
|
|
370
|
+
currentSignature: {
|
|
371
|
+
get() { return currentPoseSignature(this.pose); },
|
|
372
|
+
enumerable: true,
|
|
373
|
+
},
|
|
374
|
+
sweptSignature: {
|
|
375
|
+
get() { return sweptPoseSignature(this.pose); },
|
|
376
|
+
enumerable: true,
|
|
377
|
+
},
|
|
378
|
+
signature: {
|
|
379
|
+
get() { return sweptPoseSignature(this.pose); },
|
|
380
|
+
enumerable: true,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
return state;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const PREVIOUS_GEOMETRY_POSE_SCRATCH = { x: 0, y: 0, heading: 0 };
|
|
387
|
+
|
|
388
|
+
export function createVehicleGeometryState(car, target = null) {
|
|
389
|
+
const state = target ?? createGeometryStateShell();
|
|
390
|
+
const pose = state.pose = writeGeometryPose(state.pose, car);
|
|
391
|
+
const current = state.current = writeVehicleGeometry(state.current, pose);
|
|
392
|
+
PREVIOUS_GEOMETRY_POSE_SCRATCH.x = pose.previousX;
|
|
393
|
+
PREVIOUS_GEOMETRY_POSE_SCRATCH.y = pose.previousY;
|
|
394
|
+
PREVIOUS_GEOMETRY_POSE_SCRATCH.heading = pose.previousHeading;
|
|
395
|
+
const previous = state.previous = writeVehicleGeometry(state.previous, PREVIOUS_GEOMETRY_POSE_SCRATCH);
|
|
396
|
+
const bodyAabb = writeVehicleShapeAabb(state.bodyAabb, current.body);
|
|
397
|
+
const previousBodyAabb = writeVehicleShapeAabb(state.previousBodyAabb, previous.body);
|
|
398
|
+
const sweptBodyAabb = state.sweptBodyAabb;
|
|
399
|
+
sweptBodyAabb.minX = Math.min(bodyAabb.minX, previousBodyAabb.minX);
|
|
400
|
+
sweptBodyAabb.maxX = Math.max(bodyAabb.maxX, previousBodyAabb.maxX);
|
|
401
|
+
sweptBodyAabb.minY = Math.min(bodyAabb.minY, previousBodyAabb.minY);
|
|
402
|
+
sweptBodyAabb.maxY = Math.max(bodyAabb.maxY, previousBodyAabb.maxY);
|
|
403
|
+
|
|
404
|
+
state.body = current.body;
|
|
405
|
+
state.wheels = current.wheels;
|
|
406
|
+
state.contactPatches = current.contactPatches;
|
|
407
|
+
return state;
|
|
375
408
|
}
|
|
376
409
|
|
|
377
410
|
export function createCurrentVehicleGeometryState(car, target = null) {
|
|
@@ -388,8 +421,13 @@ export function createCurrentVehicleGeometryState(car, target = null) {
|
|
|
388
421
|
|
|
389
422
|
export function getVehicleGeometryState(car) {
|
|
390
423
|
if (geometryStateMatches(car, car.geometryState)) return car.geometryState;
|
|
391
|
-
|
|
392
|
-
|
|
424
|
+
// Double-buffer pooled states: consumers may still hold the state returned
|
|
425
|
+
// for the previous pose within a step, so alternate between two reusable
|
|
426
|
+
// shells instead of allocating a fresh geometry tree on every pose change.
|
|
427
|
+
const next = createVehicleGeometryState(car, car._geometryStateSpare ?? null);
|
|
428
|
+
car._geometryStateSpare = car.geometryState ?? null;
|
|
429
|
+
car.geometryState = next;
|
|
430
|
+
return next;
|
|
393
431
|
}
|
|
394
432
|
|
|
395
433
|
export function getCurrentVehicleGeometryState(car) {
|
|
@@ -50,7 +50,11 @@ const SIMULATOR_SURFACE_MODEL = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export function normalizePhysicsMode(value) {
|
|
53
|
-
return value
|
|
53
|
+
return PHYSICS_MODES.includes(value) ? value : DEFAULT_PHYSICS_MODE;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isSimulatorPhysicsMode(value) {
|
|
57
|
+
return value === 'advanced';
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
function accelerationLimit(speed, surfaceGrip) {
|
|
@@ -120,7 +124,8 @@ function setPhysicsTelemetry(car, {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
export function integrateVehiclePhysics(car, controls, dt, options = {}) {
|
|
123
|
-
|
|
127
|
+
const mode = normalizePhysicsMode(options.physicsMode);
|
|
128
|
+
if (mode === 'advanced') {
|
|
124
129
|
return integrateSimulatorVehiclePhysics(car, controls, dt, options);
|
|
125
130
|
}
|
|
126
131
|
return integrateArcadeVehiclePhysics(car, controls, dt, options);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
serializeParticipantInteraction,
|
|
3
|
+
serializeParticipantInteractionCached,
|
|
4
|
+
} from '../participants/participantInteractions.js';
|
|
2
5
|
|
|
3
6
|
export function finiteOrNull(value) {
|
|
4
7
|
return Number.isFinite(value) ? value : null;
|
|
@@ -185,7 +188,7 @@ export function serializeRenderCarInto(target, car, dependencies) {
|
|
|
185
188
|
target.dnfOrder = car.dnfOrder ?? null;
|
|
186
189
|
target.outOfRace = Boolean(car.outOfRace || car.destroyed);
|
|
187
190
|
target.pitStop = serializeRenderPitStop(car.pitStop);
|
|
188
|
-
target.interaction =
|
|
191
|
+
target.interaction = serializeParticipantInteractionCached(car);
|
|
189
192
|
return target;
|
|
190
193
|
}
|
|
191
194
|
|
|
@@ -2,7 +2,7 @@ import { buildDriverPersonality } from '../driverController.js';
|
|
|
2
2
|
import { clamp, normalizeAngle, seededRange } from '../simMath.js';
|
|
3
3
|
import { offsetTrackPoint, pointAt } from '../track/trackModel.js';
|
|
4
4
|
import { kphToSimSpeed, metersToSimUnits } from '../units.js';
|
|
5
|
-
import { VEHICLE_LIMITS } from './vehiclePhysics.js';
|
|
5
|
+
import { VEHICLE_LIMITS, isSimulatorPhysicsMode } from './vehiclePhysics.js';
|
|
6
6
|
import { applyWheelSurfaceState } from './wheelSurface.js';
|
|
7
7
|
import { nearestTrackStateForCar } from '../track/trackStatePolicy.js';
|
|
8
8
|
import { clearCarDnf } from '../race/retirements.js';
|
|
@@ -73,6 +73,7 @@ export function createCar(driver, index, random, track, { standingStart = false,
|
|
|
73
73
|
downforceCoefficient: vehicle.downforceCoefficient ?? 6.1 + seededRange(random, -0.18, 0.18),
|
|
74
74
|
tireGrip: vehicle.tireGrip ?? 2.22 + racecraft * 0.28 + seededRange(random, -0.03, 0.03),
|
|
75
75
|
tireCare: vehicle.tireCare ?? 1,
|
|
76
|
+
driverModel: driver.driverModel ?? vehicle.driverModel ?? null,
|
|
76
77
|
pace,
|
|
77
78
|
racecraft,
|
|
78
79
|
personality,
|
|
@@ -170,7 +171,7 @@ export function applyExternalCarState(car, partial, context) {
|
|
|
170
171
|
}
|
|
171
172
|
car.speed = clamp(Number.isFinite(car.speed) ? car.speed : 0, 0, VEHICLE_LIMITS.maxSpeed);
|
|
172
173
|
car.heading = normalizeAngle(Number.isFinite(car.heading) ? car.heading : 0);
|
|
173
|
-
syncExternalVelocityState(car, nextPartial, physicsMode
|
|
174
|
+
syncExternalVelocityState(car, nextPartial, isSimulatorPhysicsMode(physicsMode));
|
|
174
175
|
const centerState = nearestTrackStateForCar(track, car, car, car.progress ?? car.raceDistance);
|
|
175
176
|
applyWheelSurfaceState(car, track, { centerState });
|
|
176
177
|
if (
|
|
@@ -39,8 +39,9 @@ export function createRaceCanvasMarkup({
|
|
|
39
39
|
}
|
|
40
40
|
const timingPanelId = includeTimingTower ? createTimingPanelId() : '';
|
|
41
41
|
const showPhysicsModeIndicator = debug.physicsModeIndicator === true;
|
|
42
|
-
const
|
|
43
|
-
const
|
|
42
|
+
const simulatorPhysics = physicsMode === 'advanced';
|
|
43
|
+
const physicsModeLabel = `${simulatorPhysics ? 'Advanced' : 'Arcade'} physics mode`;
|
|
44
|
+
const physicsModeClass = simulatorPhysics ? 'advanced' : 'arcade';
|
|
44
45
|
|
|
45
46
|
const body = `
|
|
46
47
|
<div class="track-canvas" data-track-canvas></div>
|