@inventure71/paddockjs 2.0.1 → 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 +176 -1
- package/INSTALL_AND_UPDATE.md +24 -4
- package/README.md +84 -512
- package/docs/architecture.md +38 -24
- package/docs/component_inventory.md +72 -0
- package/docs/composable-layouts.md +77 -0
- package/docs/concepts.md +2 -2
- package/docs/custom_model_controller.md +6 -6
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +104 -27
- package/docs/getting-started.md +75 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +18 -4
- package/docs/learnings.md +6 -6
- package/docs/rules.md +12 -8
- package/docs/sense_contract.md +1 -1
- package/docs/system_specs.md +71 -21
- package/docs/theming.md +64 -0
- package/docs/training.md +26 -19
- 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 +150 -24
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +213 -29
- 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 +262 -49
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +58 -38
- package/src/config/themeOptions.js +830 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/controllerLoop.js +17 -3
- package/src/environment/episode.js +11 -1
- package/src/environment/index.d.ts +1 -2
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/options.js +1 -1
- package/src/environment/runtime.js +44 -11
- package/src/environment/scenarios.js +1 -1
- 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 +167 -118
- package/src/environment/sensors/trackRays.js +157 -48
- package/src/index.d.ts +153 -9
- package/src/index.js +52 -3
- package/src/simulation/collisionGeometry.js +260 -96
- 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 +21 -9
- package/src/simulation/pit/pitKinematics.js +2 -2
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +79 -46
- 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 +4 -6
- package/src/simulation/race/raceStep.js +26 -14
- package/src/simulation/race/safetyCar.js +1 -1
- package/src/simulation/race/stalledDnf.js +1 -1
- 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 +138 -40
- 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 +30 -3
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +66 -33
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1468 -189
- 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 -2
- package/src/simulation/vehicle/contactResolution.js +99 -62
- 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/vehicleKinematics.js +2 -2
- package/src/simulation/vehicle/vehiclePhysics.js +45 -30
- package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
- package/src/simulation/vehicle/vehicleState.js +7 -1
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1568 -519
- 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
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildCollisionCandidatePairs,
|
|
3
|
+
collisionCandidatePairFirstIndex,
|
|
4
|
+
collisionCandidatePairSecondIndex,
|
|
5
|
+
detectVehicleCollision,
|
|
6
|
+
} from '../collisionGeometry.js';
|
|
2
7
|
import { canCollide, isCollidable } from '../participants/participantInteractions.js';
|
|
3
8
|
import { clamp, normalizeAngle } from '../simMath.js';
|
|
4
9
|
import { VEHICLE_LIMITS } from './vehiclePhysics.js';
|
|
@@ -11,54 +16,84 @@ function isPitPositionControlledCar(car) {
|
|
|
11
16
|
return status === 'entering' || status === 'queued' || status === 'servicing' || status === 'exiting';
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
function
|
|
15
|
-
return
|
|
19
|
+
function velocityComponentX(car) {
|
|
20
|
+
return Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)
|
|
21
|
+
? car.velocityX
|
|
22
|
+
: Math.cos(car.heading) * car.speed;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const forward = forwardVector(car);
|
|
23
|
-
return { x: forward.x * car.speed, y: forward.y * car.speed };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function dot(a, b) {
|
|
27
|
-
return a.x * b.x + a.y * b.y;
|
|
25
|
+
function velocityComponentY(car) {
|
|
26
|
+
return Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)
|
|
27
|
+
? car.velocityY
|
|
28
|
+
: Math.sin(car.heading) * car.speed;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function
|
|
31
|
-
const
|
|
31
|
+
function applyVelocityScalars(car, velocityX, velocityY) {
|
|
32
|
+
const rawSpeed = Math.hypot(velocityX, velocityY);
|
|
33
|
+
const speed = clamp(rawSpeed, 0, VEHICLE_LIMITS.maxSpeed);
|
|
32
34
|
if (speed <= 0) {
|
|
33
35
|
car.velocityX = 0;
|
|
34
36
|
car.velocityY = 0;
|
|
35
37
|
car.speed = 0;
|
|
36
38
|
return;
|
|
37
39
|
}
|
|
38
|
-
const scale = speed / Math.max(1e-9,
|
|
39
|
-
car.velocityX =
|
|
40
|
-
car.velocityY =
|
|
40
|
+
const scale = speed / Math.max(1e-9, rawSpeed);
|
|
41
|
+
car.velocityX = velocityX * scale;
|
|
42
|
+
car.velocityY = velocityY * scale;
|
|
41
43
|
car.speed = speed;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
function recordContactVelocityResponse(stats) {
|
|
47
|
+
if (!stats) return;
|
|
48
|
+
stats.contactVelocityResponses += 1;
|
|
49
|
+
stats.contactVelocityVectorObjectAllocations += 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function prepareReportedContactMarks(scratch, carCount) {
|
|
53
|
+
const requiredLength = Math.max(1, carCount * carCount);
|
|
54
|
+
if (!(scratch.reportedContactMarks instanceof Uint32Array) || scratch.reportedContactMarks.length < requiredLength) {
|
|
55
|
+
scratch.reportedContactMarks = new Uint32Array(requiredLength);
|
|
56
|
+
}
|
|
57
|
+
scratch.reportedContactEpoch = ((scratch.reportedContactEpoch ?? 0) + 1) >>> 0;
|
|
58
|
+
if (scratch.reportedContactEpoch === 0) {
|
|
59
|
+
scratch.reportedContactMarks.fill(0);
|
|
60
|
+
scratch.reportedContactEpoch = 1;
|
|
61
|
+
}
|
|
62
|
+
return scratch.reportedContactMarks;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function contactMarkIndex(firstIndex, secondIndex, carCount) {
|
|
66
|
+
const low = firstIndex < secondIndex ? firstIndex : secondIndex;
|
|
67
|
+
const high = firstIndex < secondIndex ? secondIndex : firstIndex;
|
|
68
|
+
return low * carCount + high;
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
export function resolveCollisionsForSimulation(sim) {
|
|
45
|
-
|
|
72
|
+
sim.collisionScratch ??= {};
|
|
73
|
+
const collidableCars = sim.collisionScratch.collidableCars ?? [];
|
|
74
|
+
sim.collisionScratch.collidableCars = collidableCars;
|
|
75
|
+
collidableCars.length = 0;
|
|
76
|
+
for (let index = 0; index < sim.cars.length; index += 1) {
|
|
77
|
+
const car = sim.cars[index];
|
|
78
|
+
if (isCollidable(car)) collidableCars.push(car);
|
|
79
|
+
}
|
|
46
80
|
if (collidableCars.length < 2) return;
|
|
47
|
-
const
|
|
81
|
+
const reportedContactMarks = prepareReportedContactMarks(sim.collisionScratch, collidableCars.length);
|
|
82
|
+
const reportedContactEpoch = sim.collisionScratch.reportedContactEpoch;
|
|
48
83
|
|
|
49
84
|
for (let pass = 0; pass < 3; pass += 1) {
|
|
50
|
-
const candidates = buildCollisionCandidatePairs(collidableCars, {
|
|
51
|
-
|
|
85
|
+
const candidates = buildCollisionCandidatePairs(collidableCars, {
|
|
86
|
+
trackLength: sim.track.length,
|
|
87
|
+
scratch: sim.collisionScratch,
|
|
88
|
+
});
|
|
89
|
+
for (const pair of candidates) {
|
|
90
|
+
const [first, second] = pair;
|
|
52
91
|
if (!canCollide(first, second)) continue;
|
|
53
|
-
const collision = detectVehicleCollision(first, second);
|
|
92
|
+
const collision = detectVehicleCollision(first, second, { scratch: sim.collisionScratch });
|
|
54
93
|
if (!collision) continue;
|
|
55
94
|
const firstPitControlled = isPitPositionControlledCar(first);
|
|
56
95
|
const secondPitControlled = isPitPositionControlledCar(second);
|
|
57
96
|
if (firstPitControlled && secondPitControlled) continue;
|
|
58
|
-
const stewardCollision = {
|
|
59
|
-
...collision,
|
|
60
|
-
trackLength: sim.track.length,
|
|
61
|
-
};
|
|
62
97
|
const oneCarFixed = firstPitControlled || secondPitControlled;
|
|
63
98
|
|
|
64
99
|
const correction = Math.min(
|
|
@@ -79,8 +114,8 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
79
114
|
}
|
|
80
115
|
|
|
81
116
|
if (oneCarFixed) {
|
|
82
|
-
if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode === '
|
|
83
|
-
if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode === '
|
|
117
|
+
if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode === 'advanced');
|
|
118
|
+
if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode === 'advanced');
|
|
84
119
|
} else {
|
|
85
120
|
applyContactVelocityResponse(sim, first, second, collision.axis);
|
|
86
121
|
}
|
|
@@ -100,9 +135,15 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
100
135
|
first.contactCooldown = 1;
|
|
101
136
|
second.contactCooldown = 1;
|
|
102
137
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
138
|
+
const firstPairIndex = collisionCandidatePairFirstIndex(pair);
|
|
139
|
+
const secondPairIndex = collisionCandidatePairSecondIndex(pair);
|
|
140
|
+
const canMarkContact = firstPairIndex >= 0 && secondPairIndex >= 0;
|
|
141
|
+
const contactIndex = canMarkContact
|
|
142
|
+
? contactMarkIndex(firstPairIndex, secondPairIndex, collidableCars.length)
|
|
143
|
+
: -1;
|
|
144
|
+
const alreadyReported = canMarkContact && reportedContactMarks[contactIndex] === reportedContactEpoch;
|
|
145
|
+
if (freshContact && pass === 0 && !alreadyReported) {
|
|
146
|
+
if (canMarkContact) reportedContactMarks[contactIndex] = reportedContactEpoch;
|
|
106
147
|
sim.events.unshift({
|
|
107
148
|
type: 'contact',
|
|
108
149
|
at: sim.time,
|
|
@@ -114,48 +155,44 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
114
155
|
depth: collision.depth,
|
|
115
156
|
timeOfImpact: collision.timeOfImpact,
|
|
116
157
|
});
|
|
117
|
-
sim.reviewCollision(first, second,
|
|
158
|
+
sim.reviewCollision(first, second, collision, {
|
|
159
|
+
scratch: sim.collisionScratch,
|
|
160
|
+
trackLength: sim.track.length,
|
|
161
|
+
});
|
|
118
162
|
}
|
|
119
163
|
}
|
|
120
164
|
}
|
|
121
165
|
}
|
|
122
166
|
|
|
123
|
-
export function applyContactVelocityResponse(sim, first, second, axis) {
|
|
167
|
+
export function applyContactVelocityResponse(sim, first, second, axis, options = {}) {
|
|
168
|
+
recordContactVelocityResponse(options.stats);
|
|
124
169
|
if (
|
|
125
|
-
sim.physicsMode === '
|
|
170
|
+
sim.physicsMode === 'advanced' ||
|
|
126
171
|
Number.isFinite(first.velocityX) ||
|
|
127
172
|
Number.isFinite(second.velocityX)
|
|
128
173
|
) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
174
|
+
let firstVelocityX = velocityComponentX(first);
|
|
175
|
+
let firstVelocityY = velocityComponentY(first);
|
|
176
|
+
let secondVelocityX = velocityComponentX(second);
|
|
177
|
+
let secondVelocityY = velocityComponentY(second);
|
|
178
|
+
const relativeNormalVelocity = (secondVelocityX - firstVelocityX) * axis.x +
|
|
179
|
+
(secondVelocityY - firstVelocityY) * axis.y;
|
|
135
180
|
|
|
136
181
|
if (relativeNormalVelocity < 0) {
|
|
137
182
|
const impulse = clamp(-relativeNormalVelocity * (0.34 + sim.rules.collisionRestitution), 0, 16);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
183
|
+
firstVelocityX -= axis.x * impulse;
|
|
184
|
+
firstVelocityY -= axis.y * impulse;
|
|
185
|
+
secondVelocityX += axis.x * impulse;
|
|
186
|
+
secondVelocityY += axis.y * impulse;
|
|
142
187
|
}
|
|
143
188
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
y: firstVelocity.y * 0.997,
|
|
147
|
-
});
|
|
148
|
-
applyVelocityVector(second, {
|
|
149
|
-
x: secondVelocity.x * 0.997,
|
|
150
|
-
y: secondVelocity.y * 0.997,
|
|
151
|
-
});
|
|
189
|
+
applyVelocityScalars(first, firstVelocityX * 0.997, firstVelocityY * 0.997);
|
|
190
|
+
applyVelocityScalars(second, secondVelocityX * 0.997, secondVelocityY * 0.997);
|
|
152
191
|
return;
|
|
153
192
|
}
|
|
154
193
|
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const firstNormal = dot(firstForward, axis);
|
|
158
|
-
const secondNormal = dot(secondForward, axis);
|
|
194
|
+
const firstNormal = Math.cos(first.heading) * axis.x + Math.sin(first.heading) * axis.y;
|
|
195
|
+
const secondNormal = Math.cos(second.heading) * axis.x + Math.sin(second.heading) * axis.y;
|
|
159
196
|
const relativeNormalVelocity = second.speed * secondNormal - first.speed * firstNormal;
|
|
160
197
|
|
|
161
198
|
if (relativeNormalVelocity < 0) {
|
|
@@ -170,11 +207,11 @@ export function applyContactVelocityResponse(sim, first, second, axis) {
|
|
|
170
207
|
|
|
171
208
|
function dampPitContactVelocity(car, factor, syncVelocity = false) {
|
|
172
209
|
if (syncVelocity || Number.isFinite(car.velocityX) || Number.isFinite(car.velocityY)) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
210
|
+
applyVelocityScalars(
|
|
211
|
+
car,
|
|
212
|
+
velocityComponentX(car) * factor,
|
|
213
|
+
velocityComponentY(car) * factor,
|
|
214
|
+
);
|
|
178
215
|
return;
|
|
179
216
|
}
|
|
180
217
|
car.speed = clamp(car.speed * factor, 0, VEHICLE_LIMITS.maxSpeed);
|
|
@@ -11,8 +11,13 @@ export function mainTrackSurface(track, crossTrackError) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function wheelFullyOutside(samples, trackLimit) {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
let rightOutside = true;
|
|
15
|
+
let leftOutside = true;
|
|
16
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
17
|
+
const state = samples[index];
|
|
18
|
+
if (state.inPitLane || state.signedOffset <= trackLimit) rightOutside = false;
|
|
19
|
+
if (state.inPitLane || state.signedOffset >= -trackLimit) leftOutside = false;
|
|
20
|
+
}
|
|
16
21
|
return {
|
|
17
22
|
rightOutside,
|
|
18
23
|
leftOutside,
|
|
@@ -33,50 +38,84 @@ export function wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset
|
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export function analyticWheelState(patch, centerState, track, trackLimit) {
|
|
41
|
+
return writeAnalyticWheelStateWithProjectedHalfWidth(
|
|
42
|
+
{},
|
|
43
|
+
patch,
|
|
44
|
+
centerState,
|
|
45
|
+
track,
|
|
46
|
+
trackLimit,
|
|
47
|
+
analyticWheelProjectedHalfWidth(patch, centerState),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeAnalyticWheelState(target, patch, centerState, track, trackLimit) {
|
|
52
|
+
return writeAnalyticWheelStateWithProjectedHalfWidth(
|
|
53
|
+
target,
|
|
54
|
+
patch,
|
|
55
|
+
centerState,
|
|
56
|
+
track,
|
|
57
|
+
trackLimit,
|
|
58
|
+
analyticWheelProjectedHalfWidth(patch, centerState),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function analyticWheelProjectedHalfWidth(patch, centerState) {
|
|
63
|
+
return (
|
|
64
|
+
Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
|
|
65
|
+
Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function writeAnalyticWheelStateWithProjectedHalfWidth(
|
|
70
|
+
target,
|
|
71
|
+
patch,
|
|
72
|
+
centerState,
|
|
73
|
+
track,
|
|
74
|
+
trackLimit,
|
|
75
|
+
projectedHalfWidth,
|
|
76
|
+
) {
|
|
36
77
|
const wheelCenterOffset =
|
|
37
78
|
(patch.center.x - centerState.x) * centerState.normalX +
|
|
38
79
|
(patch.center.y - centerState.y) * centerState.normalY;
|
|
39
|
-
const projectedHalfWidth =
|
|
40
|
-
Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
|
|
41
|
-
Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
|
|
42
80
|
const minimumSignedOffset = wheelCenterOffset - projectedHalfWidth;
|
|
43
81
|
const maximumSignedOffset = wheelCenterOffset + projectedHalfWidth;
|
|
44
82
|
const signedOffset = Math.abs(minimumSignedOffset) > Math.abs(maximumSignedOffset)
|
|
45
83
|
? minimumSignedOffset
|
|
46
84
|
: maximumSignedOffset;
|
|
47
|
-
const state = stateFromSignedOffset(centerState, track, signedOffset);
|
|
48
85
|
const outside = wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit);
|
|
86
|
+
const sampledStates = target.sampledStates ?? [];
|
|
87
|
+
const state = sampledStates.length === 1 ? (sampledStates[0] ?? {}) : {};
|
|
88
|
+
sampledStates.length = 1;
|
|
89
|
+
sampledStates[0] = state;
|
|
90
|
+
writeStateFromSignedOffset(state, centerState, track, signedOffset);
|
|
49
91
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
};
|
|
92
|
+
target.id = patch.id;
|
|
93
|
+
target.x = patch.center.x;
|
|
94
|
+
target.y = patch.center.y;
|
|
95
|
+
target.signedOffset = state.signedOffset;
|
|
96
|
+
target.crossTrackError = state.crossTrackError;
|
|
97
|
+
target.surface = state.surface;
|
|
98
|
+
target.onTrack = Boolean(state.onTrack);
|
|
99
|
+
target.inPitLane = false;
|
|
100
|
+
target.pitLanePart = null;
|
|
101
|
+
target.pitBoxId = null;
|
|
102
|
+
target.minimumSignedOffset = minimumSignedOffset;
|
|
103
|
+
target.maximumSignedOffset = maximumSignedOffset;
|
|
104
|
+
target.fullyOutsideWhiteLine = outside.fullyOutsideWhiteLine;
|
|
105
|
+
target.outsideSide = outside.outsideSide;
|
|
106
|
+
target.sampledStates = sampledStates;
|
|
107
|
+
return target;
|
|
67
108
|
}
|
|
68
109
|
|
|
69
|
-
function
|
|
110
|
+
function writeStateFromSignedOffset(target, centerState, track, signedOffset) {
|
|
70
111
|
const crossTrackError = Math.abs(signedOffset);
|
|
71
112
|
const surface = mainTrackSurface(track, crossTrackError);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
pitBoxId: null,
|
|
81
|
-
};
|
|
113
|
+
target.signedOffset = signedOffset;
|
|
114
|
+
target.crossTrackError = crossTrackError;
|
|
115
|
+
target.surface = surface;
|
|
116
|
+
target.onTrack = surface === 'track' || surface === 'kerb';
|
|
117
|
+
target.inPitLane = false;
|
|
118
|
+
target.pitLanePart = null;
|
|
119
|
+
target.pitBoxId = null;
|
|
120
|
+
return target;
|
|
82
121
|
}
|
|
@@ -2,10 +2,6 @@ import { metersToSimUnits } from '../units.js';
|
|
|
2
2
|
|
|
3
3
|
const PIT_CONNECTOR_FULL_SAMPLE_WINDOW = metersToSimUnits(35);
|
|
4
4
|
|
|
5
|
-
export function patchSamples(patch) {
|
|
6
|
-
return [patch.center, ...patch.corners];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
export function isNearPitConnector(track, centerState) {
|
|
10
6
|
const pitLane = track.pitLane;
|
|
11
7
|
if (!pitLane?.enabled) return false;
|
|
@@ -20,33 +16,81 @@ export function canUseAnalyticPitWheels(geometry, centerState) {
|
|
|
20
16
|
const roadWidth = Number(centerState?.pitLaneRoadWidth);
|
|
21
17
|
if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
|
|
22
18
|
const halfRoadWidth = roadWidth / 2;
|
|
23
|
-
|
|
24
|
-
const range = patchPitOffsetRange(
|
|
25
|
-
|
|
26
|
-
}
|
|
19
|
+
for (let index = 0; index < geometry.contactPatches.length; index += 1) {
|
|
20
|
+
const range = patchPitOffsetRange(geometry.contactPatches[index], centerState);
|
|
21
|
+
if (range.minimum < -halfRoadWidth - 0.001 || range.maximum > halfRoadWidth + 0.001) return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function canSkipConnectorPitChecks(track, geometry, centerState, epsilon = 0.001) {
|
|
27
|
+
const laneSide = Math.sign(track?.pitLane?.side ?? 0);
|
|
28
|
+
if (
|
|
29
|
+
!laneSide ||
|
|
30
|
+
!geometry?.contactPatches?.length ||
|
|
31
|
+
!centerState ||
|
|
32
|
+
centerState.inPitLane ||
|
|
33
|
+
!centerState.onTrack
|
|
34
|
+
) return false;
|
|
35
|
+
for (let index = 0; index < geometry.contactPatches.length; index += 1) {
|
|
36
|
+
if (!connectorPitCheckCanBeSkippedForPatch(track, geometry.contactPatches[index], centerState, epsilon)) return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function pitPatchOffsetRange(patch, centerState) {
|
|
42
|
+
return patchPitOffsetRange(patch, centerState);
|
|
27
43
|
}
|
|
28
44
|
|
|
29
|
-
export function
|
|
45
|
+
export function pitPatchInsideRoad(patch, centerState, epsilon = 0.001) {
|
|
46
|
+
const roadWidth = Number(centerState?.pitLaneRoadWidth);
|
|
47
|
+
if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
|
|
48
|
+
const halfRoadWidth = roadWidth / 2;
|
|
30
49
|
const range = patchPitOffsetRange(patch, centerState);
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
return range.minimum >= -halfRoadWidth - epsilon && range.maximum <= halfRoadWidth + epsilon;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function pitPatchOverlapsRoad(patch, centerState, epsilon = 0.001) {
|
|
54
|
+
const roadWidth = Number(centerState?.pitLaneRoadWidth);
|
|
55
|
+
if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
|
|
56
|
+
const halfRoadWidth = roadWidth / 2;
|
|
57
|
+
const range = patchPitOffsetRange(patch, centerState);
|
|
58
|
+
return range.minimum <= halfRoadWidth + epsilon && range.maximum >= -halfRoadWidth - epsilon;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function writeAnalyticPitWheelState(target, patch, centerState) {
|
|
62
|
+
const range = patchPitOffsetRange(patch, centerState);
|
|
63
|
+
const signedOffset = centerState.mainTrackSignedOffset ?? centerState.signedOffset;
|
|
64
|
+
const sampledStates = target.sampledStates ?? [];
|
|
65
|
+
const sampleState = sampledStates.length === 1 ? (sampledStates[0] ?? {}) : {};
|
|
66
|
+
sampledStates.length = 1;
|
|
67
|
+
sampledStates[0] = sampleState;
|
|
68
|
+
sampleState.signedOffset = centerState.signedOffset;
|
|
69
|
+
sampleState.crossTrackError = centerState.crossTrackError;
|
|
70
|
+
sampleState.surface = centerState.surface;
|
|
71
|
+
sampleState.onTrack = Boolean(centerState.onTrack);
|
|
72
|
+
sampleState.inPitLane = true;
|
|
73
|
+
sampleState.pitLanePart = centerState.pitLanePart ?? null;
|
|
74
|
+
sampleState.pitBoxId = centerState.pitBoxId ?? null;
|
|
75
|
+
|
|
76
|
+
target.id = patch.id;
|
|
77
|
+
target.x = patch.center.x;
|
|
78
|
+
target.y = patch.center.y;
|
|
79
|
+
target.signedOffset = signedOffset;
|
|
80
|
+
target.crossTrackError = centerState.mainTrackCrossTrackError ?? centerState.crossTrackError;
|
|
81
|
+
target.surface = centerState.surface;
|
|
82
|
+
target.onTrack = true;
|
|
83
|
+
target.inPitLane = true;
|
|
84
|
+
target.pitLanePart = centerState.pitLanePart ?? null;
|
|
85
|
+
target.pitBoxId = centerState.pitBoxId ?? null;
|
|
86
|
+
target.minimumSignedOffset = signedOffset;
|
|
87
|
+
target.maximumSignedOffset = signedOffset;
|
|
88
|
+
target.pitLaneMinimumSignedOffset = range.minimum;
|
|
89
|
+
target.pitLaneMaximumSignedOffset = range.maximum;
|
|
90
|
+
target.fullyOutsideWhiteLine = false;
|
|
91
|
+
target.outsideSide = 0;
|
|
92
|
+
target.sampledStates = sampledStates;
|
|
93
|
+
return target;
|
|
50
94
|
}
|
|
51
95
|
|
|
52
96
|
function patchPitOffsetRange(patch, centerState) {
|
|
@@ -62,6 +106,15 @@ function patchPitOffsetRange(patch, centerState) {
|
|
|
62
106
|
};
|
|
63
107
|
}
|
|
64
108
|
|
|
109
|
+
function connectorPitCheckCanBeSkippedForPatch(track, patch, centerState, epsilon) {
|
|
110
|
+
const laneSide = Math.sign(track?.pitLane?.side ?? 0);
|
|
111
|
+
if (!laneSide || !patch || !centerState || centerState.inPitLane || !centerState.onTrack) return false;
|
|
112
|
+
const range = patchPitOffsetRange(patch, centerState);
|
|
113
|
+
return laneSide > 0
|
|
114
|
+
? range.maximum <= epsilon
|
|
115
|
+
: range.minimum >= -epsilon;
|
|
116
|
+
}
|
|
117
|
+
|
|
65
118
|
function wrappedDistanceDelta(first, second, length) {
|
|
66
119
|
if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(length) || length <= 0) return Infinity;
|
|
67
120
|
const delta = Math.abs(first - second);
|
|
@@ -1,58 +1,83 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
VEHICLE_GEOMETRY,
|
|
3
|
+
vehicleAxes,
|
|
4
|
+
} from './vehicleGeometry.js';
|
|
2
5
|
import { applyWheelSurfaceState } from './wheelSurface.js';
|
|
3
6
|
import { freezeVehicleMotion } from './vehicleKinematics.js';
|
|
4
|
-
import {
|
|
7
|
+
import { queryRunoffTrackStateForCar } from '../track/trackStatePolicy.js';
|
|
5
8
|
import { markCarDnf } from '../race/retirements.js';
|
|
6
9
|
|
|
10
|
+
export function clearRunoffCenterState(car) {
|
|
11
|
+
if (car) car.pendingRunoffCenterState = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function storeRunoffCenterState(car, state) {
|
|
15
|
+
car.pendingRunoffCenterState = {
|
|
16
|
+
state,
|
|
17
|
+
x: car.x,
|
|
18
|
+
y: car.y,
|
|
19
|
+
heading: car.heading,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function takeValidRunoffCenterState(car) {
|
|
24
|
+
const pending = car?.pendingRunoffCenterState;
|
|
25
|
+
if (!pending) return null;
|
|
26
|
+
car.pendingRunoffCenterState = null;
|
|
27
|
+
return pending.x === car.x &&
|
|
28
|
+
pending.y === car.y &&
|
|
29
|
+
pending.heading === car.heading
|
|
30
|
+
? pending.state
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
|
|
7
34
|
export function applyRunoffResponseForSimulation(sim, car) {
|
|
8
35
|
if (car.destroyed) {
|
|
36
|
+
clearRunoffCenterState(car);
|
|
9
37
|
freezeBarrierMotion(car, sim.physicsMode);
|
|
10
38
|
applyWheelSurfaceState(car, sim.track);
|
|
11
39
|
return;
|
|
12
40
|
}
|
|
13
|
-
const
|
|
14
|
-
const state = nearestTrackStateForCar(sim.track, car, car, car.progress, { allowPitOverride });
|
|
15
|
-
const mainTrackState = state.inPitLane
|
|
16
|
-
? nearestTrackStateForCar(sim.track, car, car, car.progress, { allowPitOverride: false })
|
|
17
|
-
: state;
|
|
41
|
+
const { state, mainTrackState } = queryRunoffTrackStateForCar(sim.track, car);
|
|
18
42
|
const barrierCenter = sim.track.width / 2 + (sim.track.kerbWidth ?? 0) + sim.track.gravelWidth + sim.track.runoffWidth;
|
|
19
43
|
const signedLimit = barrierCenter - (sim.track.barrierWidth ?? 0) / 2;
|
|
20
44
|
const side = Math.sign(mainTrackState.signedOffset) || 1;
|
|
21
45
|
const outwardReach = getOutwardVehicleReach(car, mainTrackState, side);
|
|
22
46
|
const overshoot = Math.abs(mainTrackState.signedOffset) + outwardReach - signedLimit;
|
|
23
47
|
if (overshoot <= 0) {
|
|
24
|
-
|
|
25
|
-
applyWheelSurfaceState(car, sim.track, { centerState: state });
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
applyWheelSurfaceState(car, sim.track, { centerState: state });
|
|
48
|
+
storeRunoffCenterState(car, state);
|
|
29
49
|
return;
|
|
30
50
|
}
|
|
31
51
|
|
|
52
|
+
clearRunoffCenterState(car);
|
|
32
53
|
destroyCarOnBarrier(sim, car);
|
|
33
54
|
applyWheelSurfaceState(car, sim.track, { centerState: mainTrackState });
|
|
34
55
|
}
|
|
35
56
|
|
|
36
57
|
function getOutwardVehicleReach(car, state, side) {
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
reach = Math.max(reach, lateral * side);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
return reach;
|
|
58
|
+
const axes = vehicleAxes(car.heading ?? 0);
|
|
59
|
+
const forwardDot = axes.forward.x * state.normalX + axes.forward.y * state.normalY;
|
|
60
|
+
const rightDot = axes.right.x * state.normalX + axes.right.y * state.normalY;
|
|
61
|
+
return Math.max(
|
|
62
|
+
orientedHalfExtent(VEHICLE_GEOMETRY.visualLength / 2, VEHICLE_GEOMETRY.visualWidth / 2, forwardDot, rightDot),
|
|
63
|
+
orientedHalfExtent(VEHICLE_GEOMETRY.bodyLength / 2, VEHICLE_GEOMETRY.bodyWidth / 2, forwardDot, rightDot),
|
|
64
|
+
wheelOutwardReach(forwardDot, rightDot, side),
|
|
65
|
+
);
|
|
49
66
|
}
|
|
50
67
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
function orientedHalfExtent(halfLength, halfWidth, forwardDot, rightDot) {
|
|
69
|
+
return Math.abs(forwardDot) * halfLength + Math.abs(rightDot) * halfWidth;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function wheelOutwardReach(forwardDot, rightDot, side) {
|
|
73
|
+
const centerOffset =
|
|
74
|
+
Math.abs(forwardDot) * VEHICLE_GEOMETRY.wheelLongitudinalOffset +
|
|
75
|
+
Math.abs(rightDot * side) * VEHICLE_GEOMETRY.wheelLateralOffset;
|
|
76
|
+
return centerOffset + orientedHalfExtent(
|
|
77
|
+
VEHICLE_GEOMETRY.wheelLength / 2,
|
|
78
|
+
VEHICLE_GEOMETRY.wheelWidth / 2,
|
|
79
|
+
forwardDot,
|
|
80
|
+
rightDot,
|
|
56
81
|
);
|
|
57
82
|
}
|
|
58
83
|
|