@inventure71/paddockjs 3.0.0 → 4.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 +146 -1
- package/INSTALL_AND_UPDATE.md +15 -4
- package/README.md +84 -562
- package/docs/architecture.md +28 -18
- package/docs/composable-layouts.md +77 -0
- package/docs/custom_model_controller.md +4 -4
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +9 -7
- package/docs/getting-started.md +75 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +17 -4
- package/docs/rules.md +6 -2
- package/docs/system_specs.md +49 -6
- package/docs/theming.md +64 -0
- package/docs/training.md +20 -14
- package/docs/troubleshooting.md +76 -0
- package/docs/upgrading-hosts-to-3.md +42 -0
- package/examples/python/README.md +6 -0
- package/examples/python/base_policy_server.py +19 -0
- package/package.json +10 -3
- package/src/api/PaddockSimulatorController.js +78 -22
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +24 -3
- package/src/app/readouts/timingTowerRenderer.js +203 -42
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/config/defaultOptions.js +8 -1
- package/src/config/themeOptions.js +5 -0
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/episode.js +11 -1
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/runtime.js +41 -0
- package/src/environment/sensors/carRays.js +48 -37
- package/src/environment/sensors/index.js +501 -97
- package/src/environment/sensors/indexedRayBands.js +134 -46
- package/src/environment/sensors/rayBandTrace.js +1171 -0
- package/src/environment/sensors/rayChannels.js +108 -0
- package/src/environment/sensors/rayConfig.js +71 -25
- package/src/environment/sensors/rayGeometry.js +23 -17
- package/src/environment/sensors/sensorTargets.js +67 -1
- package/src/environment/sensors/surfaceRays.js +130 -89
- package/src/environment/sensors/trackRays.js +115 -1
- package/src/index.d.ts +43 -0
- package/src/index.js +27 -2
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +77 -42
- package/src/simulation/pit/pitState.js +1 -0
- package/src/simulation/pit/pitTireService.js +7 -3
- package/src/simulation/race/raceFinish.js +25 -8
- package/src/simulation/race/raceOrder.js +189 -32
- package/src/simulation/race/raceProgress.js +129 -37
- package/src/simulation/race/raceSetup.js +2 -0
- package/src/simulation/race/raceStep.js +25 -13
- package/src/simulation/raceSimulation.js +20 -17
- package/src/simulation/rules/collisionSteward.js +40 -23
- package/src/simulation/rules/rulesReview.js +77 -56
- package/src/simulation/snapshots/raceSnapshots.js +37 -0
- package/src/simulation/timing/drsTiming.js +143 -30
- package/src/simulation/timing/gapEstimation.js +25 -3
- package/src/simulation/timing/lapTelemetry.js +115 -36
- package/src/simulation/timing/raceTiming.js +5 -1
- package/src/simulation/timing/sectorPerformance.js +104 -26
- package/src/simulation/timing/timingHistory.js +229 -14
- package/src/simulation/timing/timingLines.js +65 -7
- package/src/simulation/track/pitLaneLayout.js +23 -0
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +67 -11
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1334 -100
- package/src/simulation/track/trackQueryPitIndex.js +138 -14
- package/src/simulation/track/trackQueryScratch.js +7 -0
- package/src/simulation/track/trackQueryStats.js +25 -0
- package/src/simulation/track/trackSnapshotJson.js +139 -0
- package/src/simulation/track/trackStatePolicy.js +249 -1
- package/src/simulation/vehicle/contactResolution.js +96 -59
- package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
- package/src/simulation/vehicle/pitWheelSurface.js +81 -28
- package/src/simulation/vehicle/runoffResponse.js +54 -29
- package/src/simulation/vehicle/vehicleGeometry.js +245 -51
- package/src/simulation/vehicle/vehiclePhysics.js +42 -27
- package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
- package/src/simulation/vehicle/vehicleState.js +6 -0
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/styles.css +497 -497
|
@@ -1,35 +1,40 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createVehicleGeometry,
|
|
3
|
-
createVehicleGeometryState,
|
|
4
2
|
getVehicleGeometryState,
|
|
5
|
-
|
|
3
|
+
vehicleAxes,
|
|
6
4
|
} from './vehicle/vehicleGeometry.js';
|
|
5
|
+
import { normalizeAngle } from './simMath.js';
|
|
7
6
|
|
|
8
7
|
const DEFAULT_SWEEP_STEPS = 16;
|
|
9
8
|
const BROADPHASE_PADDING = 0.001;
|
|
10
9
|
const DEFAULT_DISTANCE_WINDOW = 150;
|
|
10
|
+
const COLLISION_PAIR_FIRST_INDEX = Symbol('collisionPairFirstIndex');
|
|
11
|
+
const COLLISION_PAIR_SECOND_INDEX = Symbol('collisionPairSecondIndex');
|
|
11
12
|
|
|
12
13
|
function dot(a, b) {
|
|
13
14
|
return a.x * b.x + a.y * b.y;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
+
function projectShapeInto(target, shape, axis) {
|
|
17
18
|
let min = Infinity;
|
|
18
19
|
let max = -Infinity;
|
|
19
20
|
for (let index = 0; index < shape.corners.length; index += 1) {
|
|
20
|
-
const
|
|
21
|
+
const corner = shape.corners[index];
|
|
22
|
+
const value = corner.x * axis.x + corner.y * axis.y;
|
|
21
23
|
if (value < min) min = value;
|
|
22
24
|
if (value > max) max = value;
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
26
|
+
target.min = min;
|
|
27
|
+
target.max = max;
|
|
28
|
+
return target;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function overlapOnAxis(first, second, axis) {
|
|
31
|
-
const
|
|
32
|
-
|
|
31
|
+
function overlapOnAxis(first, second, axis, projectionScratch = null) {
|
|
32
|
+
const projections = projectionScratch ?? {
|
|
33
|
+
first: { min: Infinity, max: -Infinity },
|
|
34
|
+
second: { min: Infinity, max: -Infinity },
|
|
35
|
+
};
|
|
36
|
+
const a = projectShapeInto(projections.first, first, axis);
|
|
37
|
+
const b = projectShapeInto(projections.second, second, axis);
|
|
33
38
|
return Math.min(a.max, b.max) - Math.max(a.min, b.min);
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -37,58 +42,44 @@ function shapeContactType(firstShape, secondShape) {
|
|
|
37
42
|
return `${firstShape.type}-${secondShape.type}`;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
export function detectShapeCollision(firstShape, secondShape) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
secondShape.right,
|
|
46
|
-
];
|
|
47
|
-
let depth = Infinity;
|
|
48
|
-
let axis = null;
|
|
49
|
-
|
|
50
|
-
for (const candidate of axes) {
|
|
51
|
-
const overlap = overlapOnAxis(firstShape, secondShape, candidate);
|
|
52
|
-
if (overlap <= 0) return null;
|
|
53
|
-
if (overlap < depth) {
|
|
54
|
-
depth = overlap;
|
|
55
|
-
axis = candidate;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
45
|
+
export function detectShapeCollision(firstShape, secondShape, scratch = null) {
|
|
46
|
+
const projectionScratch = ensureProjectionScratch(scratch);
|
|
47
|
+
let axis = firstShape.forward;
|
|
48
|
+
let depth = overlapOnAxis(firstShape, secondShape, axis, projectionScratch);
|
|
49
|
+
if (depth <= 0) return null;
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
axis = { x: -axis.x, y: -axis.y };
|
|
51
|
+
let overlap = overlapOnAxis(firstShape, secondShape, firstShape.right, projectionScratch);
|
|
52
|
+
if (overlap <= 0) return null;
|
|
53
|
+
if (overlap < depth) {
|
|
54
|
+
depth = overlap;
|
|
55
|
+
axis = firstShape.right;
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
74
|
-
}
|
|
58
|
+
overlap = overlapOnAxis(firstShape, secondShape, secondShape.forward, projectionScratch);
|
|
59
|
+
if (overlap <= 0) return null;
|
|
60
|
+
if (overlap < depth) {
|
|
61
|
+
depth = overlap;
|
|
62
|
+
axis = secondShape.forward;
|
|
63
|
+
}
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
return
|
|
78
|
-
|
|
65
|
+
overlap = overlapOnAxis(firstShape, secondShape, secondShape.right, projectionScratch);
|
|
66
|
+
if (overlap <= 0) return null;
|
|
67
|
+
if (overlap < depth) {
|
|
68
|
+
depth = overlap;
|
|
69
|
+
axis = secondShape.right;
|
|
70
|
+
}
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return best;
|
|
72
|
+
const directionX = secondShape.center.x - firstShape.center.x;
|
|
73
|
+
const directionY = secondShape.center.y - firstShape.center.y;
|
|
74
|
+
const directionSign = directionX * axis.x + directionY * axis.y < 0 ? -1 : 1;
|
|
75
|
+
return writeShapeCollisionResult(
|
|
76
|
+
scratch ? (scratch.shapeCollisionResult ??= { axis: {} }) : { axis: {} },
|
|
77
|
+
firstShape,
|
|
78
|
+
secondShape,
|
|
79
|
+
axis.x * directionSign,
|
|
80
|
+
axis.y * directionSign,
|
|
81
|
+
depth,
|
|
82
|
+
);
|
|
92
83
|
}
|
|
93
84
|
|
|
94
85
|
function aabbsOverlap(first, second) {
|
|
@@ -104,34 +95,60 @@ function createSweptAabb(car) {
|
|
|
104
95
|
return getVehicleGeometryState(car).sweptBodyAabb;
|
|
105
96
|
}
|
|
106
97
|
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
function writeShapeCollisionResult(target, firstShape, secondShape, axisX, axisY, depth) {
|
|
99
|
+
target.axis ??= {};
|
|
100
|
+
target.axis.x = axisX;
|
|
101
|
+
target.axis.y = axisY;
|
|
102
|
+
target.depth = depth;
|
|
103
|
+
target.firstShapeId = firstShape.id;
|
|
104
|
+
target.secondShapeId = secondShape.id;
|
|
105
|
+
target.contactType = shapeContactType(firstShape, secondShape);
|
|
106
|
+
return target;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function withCollisionMetadata(collision, timeOfImpact, swept, scratch) {
|
|
110
|
+
const target = scratch ? (scratch.vehicleCollisionResult ??= { axis: {} }) : { axis: {} };
|
|
111
|
+
target.axis ??= {};
|
|
112
|
+
target.axis.x = collision.axis.x;
|
|
113
|
+
target.axis.y = collision.axis.y;
|
|
114
|
+
target.depth = collision.depth;
|
|
115
|
+
target.firstShapeId = collision.firstShapeId;
|
|
116
|
+
target.secondShapeId = collision.secondShapeId;
|
|
117
|
+
target.contactType = collision.contactType;
|
|
118
|
+
target.timeOfImpact = timeOfImpact;
|
|
119
|
+
target.swept = Boolean(swept);
|
|
120
|
+
return target;
|
|
113
121
|
}
|
|
114
122
|
|
|
115
|
-
|
|
123
|
+
function detectVehicleCollisionWithScratch(first, second, sweepSteps, scratch) {
|
|
116
124
|
const firstState = getVehicleGeometryState(first);
|
|
117
125
|
const secondState = getVehicleGeometryState(second);
|
|
118
|
-
const current =
|
|
119
|
-
if (current) return withCollisionMetadata(current, 1, false);
|
|
126
|
+
const current = detectShapeCollision(firstState.current.body, secondState.current.body, scratch);
|
|
127
|
+
if (current) return withCollisionMetadata(current, 1, false, scratch);
|
|
120
128
|
|
|
121
129
|
if (!aabbsOverlap(createSweptAabb(first), createSweptAabb(second))) return null;
|
|
122
130
|
|
|
131
|
+
const sweepShapes = ensureSweepShapes(scratch, firstState.current.body, secondState.current.body);
|
|
123
132
|
for (let step = 1; step < sweepSteps; step += 1) {
|
|
124
133
|
const amount = step / sweepSteps;
|
|
125
|
-
const collision =
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
const collision = detectShapeCollision(
|
|
135
|
+
writeInterpolatedBodyShape(sweepShapes.first, firstState, amount),
|
|
136
|
+
writeInterpolatedBodyShape(sweepShapes.second, secondState, amount),
|
|
137
|
+
scratch,
|
|
128
138
|
);
|
|
129
|
-
if (collision) return withCollisionMetadata(collision, amount, true);
|
|
139
|
+
if (collision) return withCollisionMetadata(collision, amount, true, scratch);
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
return null;
|
|
133
143
|
}
|
|
134
144
|
|
|
145
|
+
export function detectVehicleCollision(first, second, {
|
|
146
|
+
sweepSteps = DEFAULT_SWEEP_STEPS,
|
|
147
|
+
scratch = null,
|
|
148
|
+
} = {}) {
|
|
149
|
+
return detectVehicleCollisionWithScratch(first, second, sweepSteps, scratch);
|
|
150
|
+
}
|
|
151
|
+
|
|
135
152
|
function normalizedDistance(car, trackLength) {
|
|
136
153
|
if (!Number.isFinite(trackLength) || trackLength <= 0) return car.raceDistance;
|
|
137
154
|
const distance = Number.isFinite(car.progress) ? car.progress : car.raceDistance;
|
|
@@ -147,34 +164,51 @@ function wrappedDistanceDelta(first, second, trackLength) {
|
|
|
147
164
|
export function buildCollisionCandidatePairs(cars, {
|
|
148
165
|
trackLength = null,
|
|
149
166
|
distanceWindow = DEFAULT_DISTANCE_WINDOW,
|
|
167
|
+
scratch = null,
|
|
150
168
|
} = {}) {
|
|
151
|
-
const candidates =
|
|
152
|
-
const
|
|
153
|
-
const withDistance =
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
const candidates = scratchArray(scratch, 'candidates');
|
|
170
|
+
const pairMarker = candidatePairMarker(scratch, cars.length);
|
|
171
|
+
const withDistance = scratchArray(scratch, 'withDistance');
|
|
172
|
+
const pairPool = scratchArray(scratch, 'pairPool', { clear: false });
|
|
173
|
+
const distanceEntryPool = scratchArray(scratch, 'distanceEntryPool', { clear: false });
|
|
174
|
+
const missingDistanceFlags = missingDistanceFlagScratch(scratch, cars.length);
|
|
175
|
+
const missingDistanceIndexes = scratchArray(scratch, 'missingDistanceIndexes');
|
|
176
|
+
|
|
177
|
+
const addCandidateByIndex = (firstIndex, secondIndex) => {
|
|
178
|
+
if (pairMarker.has(firstIndex, secondIndex)) return;
|
|
179
|
+
pairMarker.add(firstIndex, secondIndex);
|
|
180
|
+
const pair = pairPool[candidates.length] ?? [];
|
|
181
|
+
pair[0] = cars[firstIndex];
|
|
182
|
+
pair[1] = cars[secondIndex];
|
|
183
|
+
pair[COLLISION_PAIR_FIRST_INDEX] = firstIndex;
|
|
184
|
+
pair[COLLISION_PAIR_SECOND_INDEX] = secondIndex;
|
|
185
|
+
pairPool[candidates.length] = pair;
|
|
186
|
+
candidates.push(pair);
|
|
164
187
|
};
|
|
165
188
|
|
|
166
|
-
cars.
|
|
189
|
+
for (let index = 0; index < cars.length; index += 1) {
|
|
190
|
+
const car = cars[index];
|
|
191
|
+
missingDistanceFlags[index] = 0;
|
|
167
192
|
const distance = normalizedDistance(car, trackLength);
|
|
168
|
-
if (Number.isFinite(distance))
|
|
169
|
-
|
|
170
|
-
|
|
193
|
+
if (Number.isFinite(distance)) {
|
|
194
|
+
const entry = distanceEntryPool[withDistance.length] ?? {};
|
|
195
|
+
entry.car = car;
|
|
196
|
+
entry.carIndex = index;
|
|
197
|
+
entry.distance = distance;
|
|
198
|
+
distanceEntryPool[withDistance.length] = entry;
|
|
199
|
+
withDistance.push(entry);
|
|
200
|
+
} else {
|
|
201
|
+
missingDistanceFlags[index] = 1;
|
|
202
|
+
missingDistanceIndexes.push(index);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
171
205
|
|
|
172
206
|
withDistance.sort((first, second) => first.distance - second.distance);
|
|
173
207
|
for (let i = 0; i < withDistance.length; i += 1) {
|
|
174
208
|
for (let j = i + 1; j < withDistance.length; j += 1) {
|
|
175
209
|
const delta = withDistance[j].distance - withDistance[i].distance;
|
|
176
210
|
if (delta > distanceWindow) break;
|
|
177
|
-
|
|
211
|
+
addCandidateByIndex(withDistance[i].carIndex, withDistance[j].carIndex);
|
|
178
212
|
}
|
|
179
213
|
}
|
|
180
214
|
|
|
@@ -184,17 +218,18 @@ export function buildCollisionCandidatePairs(cars, {
|
|
|
184
218
|
const firstWrappedIndex = lowerBoundDistance(withDistance, threshold);
|
|
185
219
|
for (let j = Math.max(firstWrappedIndex, i + 1); j < withDistance.length; j += 1) {
|
|
186
220
|
if (wrappedDistanceDelta(withDistance[i].distance, withDistance[j].distance, trackLength) <= distanceWindow) {
|
|
187
|
-
|
|
221
|
+
addCandidateByIndex(withDistance[i].carIndex, withDistance[j].carIndex);
|
|
188
222
|
}
|
|
189
223
|
}
|
|
190
224
|
}
|
|
191
225
|
}
|
|
192
226
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
227
|
+
if (missingDistanceIndexes.length > 0) {
|
|
228
|
+
for (let i = 0; i < cars.length; i += 1) {
|
|
229
|
+
for (let j = i + 1; j < cars.length; j += 1) {
|
|
230
|
+
if (missingDistanceFlags[i] || missingDistanceFlags[j]) {
|
|
231
|
+
addCandidateByIndex(i, j);
|
|
232
|
+
}
|
|
198
233
|
}
|
|
199
234
|
}
|
|
200
235
|
}
|
|
@@ -202,6 +237,135 @@ export function buildCollisionCandidatePairs(cars, {
|
|
|
202
237
|
return candidates;
|
|
203
238
|
}
|
|
204
239
|
|
|
240
|
+
export function collisionCandidatePairFirstIndex(pair) {
|
|
241
|
+
return Number.isInteger(pair?.[COLLISION_PAIR_FIRST_INDEX])
|
|
242
|
+
? pair[COLLISION_PAIR_FIRST_INDEX]
|
|
243
|
+
: -1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function collisionCandidatePairSecondIndex(pair) {
|
|
247
|
+
return Number.isInteger(pair?.[COLLISION_PAIR_SECOND_INDEX])
|
|
248
|
+
? pair[COLLISION_PAIR_SECOND_INDEX]
|
|
249
|
+
: -1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function scratchArray(scratch, key, { clear = true } = {}) {
|
|
253
|
+
const array = scratch ? (scratch[key] ??= []) : [];
|
|
254
|
+
if (clear) array.length = 0;
|
|
255
|
+
return array;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function candidatePairMarker(scratch, carCount) {
|
|
259
|
+
if (!scratch) return setBackedCandidatePairMarker(carCount);
|
|
260
|
+
const requiredLength = Math.max(1, carCount * carCount);
|
|
261
|
+
if (!(scratch.candidatePairMarks instanceof Uint32Array) || scratch.candidatePairMarks.length < requiredLength) {
|
|
262
|
+
scratch.candidatePairMarks = new Uint32Array(requiredLength);
|
|
263
|
+
}
|
|
264
|
+
scratch.candidatePairEpoch = ((scratch.candidatePairEpoch ?? 0) + 1) >>> 0;
|
|
265
|
+
if (scratch.candidatePairEpoch === 0) {
|
|
266
|
+
scratch.candidatePairMarks.fill(0);
|
|
267
|
+
scratch.candidatePairEpoch = 1;
|
|
268
|
+
}
|
|
269
|
+
const marks = scratch.candidatePairMarks;
|
|
270
|
+
const epoch = scratch.candidatePairEpoch;
|
|
271
|
+
return {
|
|
272
|
+
has(firstIndex, secondIndex) {
|
|
273
|
+
return marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] === epoch;
|
|
274
|
+
},
|
|
275
|
+
add(firstIndex, secondIndex) {
|
|
276
|
+
marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] = epoch;
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function missingDistanceFlagScratch(scratch, carCount) {
|
|
282
|
+
if (!scratch) return new Uint8Array(Math.max(1, carCount));
|
|
283
|
+
if (!(scratch.missingDistanceFlags instanceof Uint8Array) || scratch.missingDistanceFlags.length < carCount) {
|
|
284
|
+
scratch.missingDistanceFlags = new Uint8Array(Math.max(1, carCount));
|
|
285
|
+
}
|
|
286
|
+
return scratch.missingDistanceFlags;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function setBackedCandidatePairMarker(carCount) {
|
|
290
|
+
const marks = new Uint32Array(Math.max(1, carCount * carCount));
|
|
291
|
+
return {
|
|
292
|
+
has(firstIndex, secondIndex) {
|
|
293
|
+
return marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] === 1;
|
|
294
|
+
},
|
|
295
|
+
add(firstIndex, secondIndex) {
|
|
296
|
+
marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] = 1;
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function candidatePairMarkIndex(firstIndex, secondIndex, carCount) {
|
|
302
|
+
const low = firstIndex < secondIndex ? firstIndex : secondIndex;
|
|
303
|
+
const high = firstIndex < secondIndex ? secondIndex : firstIndex;
|
|
304
|
+
return low * carCount + high;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function ensureSweepShapes(scratch, firstBody, secondBody) {
|
|
308
|
+
const sweepShapes = scratch ? (scratch.sweepShapes ??= {}) : {};
|
|
309
|
+
sweepShapes.first ??= createSweepShape(firstBody);
|
|
310
|
+
sweepShapes.second ??= createSweepShape(secondBody);
|
|
311
|
+
return sweepShapes;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function ensureProjectionScratch(scratch) {
|
|
315
|
+
const projectionScratch = scratch ? (scratch.projectionScratch ??= {}) : {};
|
|
316
|
+
projectionScratch.first ??= { min: Infinity, max: -Infinity };
|
|
317
|
+
projectionScratch.second ??= { min: Infinity, max: -Infinity };
|
|
318
|
+
return projectionScratch;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function createSweepShape(body) {
|
|
322
|
+
return {
|
|
323
|
+
id: body.id,
|
|
324
|
+
type: body.type,
|
|
325
|
+
center: { x: body.center.x, y: body.center.y },
|
|
326
|
+
heading: body.heading,
|
|
327
|
+
length: body.length,
|
|
328
|
+
width: body.width,
|
|
329
|
+
halfLength: body.halfLength,
|
|
330
|
+
halfWidth: body.halfWidth,
|
|
331
|
+
forward: { x: body.forward.x, y: body.forward.y },
|
|
332
|
+
right: { x: body.right.x, y: body.right.y },
|
|
333
|
+
corners: Array.from({ length: 4 }, () => ({ x: 0, y: 0 })),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function writeInterpolatedBodyShape(target, geometryState, amount) {
|
|
338
|
+
const pose = geometryState.pose;
|
|
339
|
+
const headingDelta = normalizeAngle(pose.heading - pose.previousHeading);
|
|
340
|
+
const x = pose.previousX + (pose.x - pose.previousX) * amount;
|
|
341
|
+
const y = pose.previousY + (pose.y - pose.previousY) * amount;
|
|
342
|
+
const heading = normalizeAngle(pose.previousHeading + headingDelta * amount);
|
|
343
|
+
const axes = vehicleAxes(heading);
|
|
344
|
+
const halfLength = target.halfLength;
|
|
345
|
+
const halfWidth = target.halfWidth;
|
|
346
|
+
|
|
347
|
+
target.center.x = x;
|
|
348
|
+
target.center.y = y;
|
|
349
|
+
target.heading = heading;
|
|
350
|
+
target.forward.x = axes.forward.x;
|
|
351
|
+
target.forward.y = axes.forward.y;
|
|
352
|
+
target.right.x = axes.right.x;
|
|
353
|
+
target.right.y = axes.right.y;
|
|
354
|
+
|
|
355
|
+
writeOffsetCorner(target.corners[0], target.center, target.forward, target.right, halfLength, halfWidth);
|
|
356
|
+
writeOffsetCorner(target.corners[1], target.center, target.forward, target.right, halfLength, -halfWidth);
|
|
357
|
+
writeOffsetCorner(target.corners[2], target.center, target.forward, target.right, -halfLength, -halfWidth);
|
|
358
|
+
writeOffsetCorner(target.corners[3], target.center, target.forward, target.right, -halfLength, halfWidth);
|
|
359
|
+
|
|
360
|
+
return target;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function writeOffsetCorner(target, center, forward, right, longitudinal, lateral) {
|
|
364
|
+
target.x = center.x + forward.x * longitudinal + right.x * lateral;
|
|
365
|
+
target.y = center.y + forward.y * longitudinal + right.y * lateral;
|
|
366
|
+
return target;
|
|
367
|
+
}
|
|
368
|
+
|
|
205
369
|
function lowerBoundDistance(entries, target) {
|
|
206
370
|
let low = 0;
|
|
207
371
|
let high = entries.length;
|
|
@@ -78,9 +78,14 @@ export function canStartPitStop(sim, car) {
|
|
|
78
78
|
const stop = car.pitStop;
|
|
79
79
|
const pitStops = sim.rules.modules?.pitStops;
|
|
80
80
|
if (!stop || !pitStops?.enabled) return false;
|
|
81
|
-
const active = sim.cars.filter((candidate) => candidate !== car && isCarInActivePitStop(sim, candidate));
|
|
82
81
|
const maxConcurrentPitLaneCars = Math.max(1, Math.floor(pitStops.maxConcurrentPitLaneCars ?? 3));
|
|
83
|
-
|
|
82
|
+
let activeCount = 0;
|
|
83
|
+
for (let index = 0; index < sim.cars.length; index += 1) {
|
|
84
|
+
const candidate = sim.cars[index];
|
|
85
|
+
if (candidate === car || !isCarInActivePitStop(sim, candidate)) continue;
|
|
86
|
+
activeCount += 1;
|
|
87
|
+
if (activeCount >= maxConcurrentPitLaneCars) return false;
|
|
88
|
+
}
|
|
84
89
|
|
|
85
90
|
if (!pitStops.doubleStacking && stop.teamId) {
|
|
86
91
|
const box = getPitStopBox(sim, stop);
|
|
@@ -89,10 +94,13 @@ export function canStartPitStop(sim, car) {
|
|
|
89
94
|
|
|
90
95
|
const minimumGap = Math.max(0, pitStops.minimumPitLaneGap ?? 0);
|
|
91
96
|
const candidateDistance = car.raceDistance ?? 0;
|
|
92
|
-
|
|
97
|
+
for (let index = 0; index < sim.cars.length; index += 1) {
|
|
98
|
+
const candidate = sim.cars[index];
|
|
99
|
+
if (candidate === car || !isCarInActivePitStop(sim, candidate)) continue;
|
|
93
100
|
const gap = (candidate.raceDistance ?? 0) - candidateDistance;
|
|
94
|
-
|
|
95
|
-
}
|
|
101
|
+
if (gap >= 0 && gap < minimumGap) return false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
export function shouldStartPitStop(sim, car) {
|
|
@@ -152,13 +160,17 @@ export function startPitStop(sim, car) {
|
|
|
152
160
|
0,
|
|
153
161
|
(hasServiceQueue ? queueDistanceAlongLane : stopDistanceAlongLane) - PIT_BOX_APPROACH_DISTANCE,
|
|
154
162
|
);
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
const routePoints = createPitApproachPoints(sim.track, car, pitLane, stop.entryRaceDistance);
|
|
164
|
+
const entryCenterline = pitLane.entry.roadCenterline ?? [];
|
|
165
|
+
for (let index = 0; index < entryCenterline.length; index += 1) {
|
|
166
|
+
routePoints.push(routePoint(entryCenterline[index]));
|
|
167
|
+
}
|
|
168
|
+
routePoints.push(
|
|
158
169
|
routePoint(pitMainLanePointAt(pitLane, 0, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
159
170
|
routePoint(pitMainLanePointAt(pitLane, boxApproachDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
|
|
160
171
|
routePoint(serviceTarget, pitLane.mainLane.heading, { limiterActive: true }),
|
|
161
|
-
|
|
172
|
+
);
|
|
173
|
+
const route = createRoute(routePoints);
|
|
162
174
|
stop.status = 'entering';
|
|
163
175
|
stop.phase = 'entry';
|
|
164
176
|
stop.queueingForService = shouldStageForService;
|
|
@@ -34,11 +34,13 @@ export function beginPitQueue(sim, car, box) {
|
|
|
34
34
|
export function releasePitQueue(sim, car, box, { fromCurrent = false } = {}) {
|
|
35
35
|
const stop = car.pitStop;
|
|
36
36
|
if (!stop || !box?.center) return false;
|
|
37
|
-
const
|
|
38
|
-
|
|
37
|
+
const routePoints = [];
|
|
38
|
+
if (fromCurrent) routePoints.push(routePoint(car, car.heading, { limiterActive: true }));
|
|
39
|
+
routePoints.push(
|
|
39
40
|
routePoint(box.queuePoint, sim.track.pitLane.mainLane.heading, { limiterActive: true }),
|
|
40
41
|
routePoint(box.center, sim.track.pitLane.mainLane.heading, { limiterActive: true }),
|
|
41
|
-
|
|
42
|
+
);
|
|
43
|
+
const route = createRoute(routePoints);
|
|
42
44
|
stop.status = 'entering';
|
|
43
45
|
stop.phase = 'queue-release';
|
|
44
46
|
stop.queueingForService = false;
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
distanceToNextLimiterSegment,
|
|
7
7
|
nearestDistanceOnRoute,
|
|
8
8
|
routeLimiterActiveAt,
|
|
9
|
-
|
|
9
|
+
sampleRouteInto,
|
|
10
10
|
} from './pitRouting.js';
|
|
11
11
|
import { syncPitCarKinematics } from './pitKinematics.js';
|
|
12
12
|
import { completePitPenaltyService } from './pitPenaltyService.js';
|
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
pointDistance,
|
|
27
27
|
} from './pitServiceConstants.js';
|
|
28
28
|
|
|
29
|
+
function samplePitStopRoute(stop, route, distanceAlong) {
|
|
30
|
+
const target = stop.routeSample ?? (stop.routeSample = { x: 0, y: 0, heading: 0, limiterActive: false });
|
|
31
|
+
return sampleRouteInto(target, route, distanceAlong);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export function applyPitRoutePosition(sim, car, delta) {
|
|
30
35
|
const stop = car.pitStop;
|
|
31
36
|
if (!stop?.route) return false;
|
|
@@ -33,7 +38,7 @@ export function applyPitRoutePosition(sim, car, delta) {
|
|
|
33
38
|
const route = stop.route;
|
|
34
39
|
if (stop.phase === 'queue-release') {
|
|
35
40
|
const nextProgress = Math.min(route.length, (stop.routeProgress ?? 0) + PIT_QUEUE_RELEASE_SPEED * delta);
|
|
36
|
-
const point =
|
|
41
|
+
const point = samplePitStopRoute(stop, route, nextProgress);
|
|
37
42
|
if (!point) return false;
|
|
38
43
|
const previousHeading = car.heading;
|
|
39
44
|
car.previousX = car.x;
|
|
@@ -63,7 +68,7 @@ export function applyPitRoutePosition(sim, car, delta) {
|
|
|
63
68
|
return route.length - stop.routeProgress <= PIT_QUEUE_RELEASE_FINISH_DISTANCE;
|
|
64
69
|
}
|
|
65
70
|
stop.routeProgress = clamp(nearestDistanceOnRoute(route, car, stop.routeProgress ?? 0), 0, route.length);
|
|
66
|
-
const routeEnd = route.points.
|
|
71
|
+
const routeEnd = route.points[route.points.length - 1];
|
|
67
72
|
const limiterActive = routeLimiterActiveAt(route, stop.routeProgress);
|
|
68
73
|
let targetSpeed = limiterActive
|
|
69
74
|
? Math.min(speedLimit, VEHICLE_LIMITS.maxSpeed)
|
|
@@ -93,7 +98,7 @@ export function applyPitRoutePosition(sim, car, delta) {
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
const nextProgress = Math.min(route.length, stop.routeProgress + Math.max(0, targetSpeed) * delta);
|
|
96
|
-
const point =
|
|
101
|
+
const point = samplePitStopRoute(stop, route, nextProgress);
|
|
97
102
|
if (!point) return false;
|
|
98
103
|
const previousSpeed = car.speed;
|
|
99
104
|
const previousHeading = car.heading;
|