@inventure71/paddockjs 1.0.0 → 2.0.1
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 +58 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +219 -12
- package/docs/architecture.md +126 -45
- package/docs/concepts.md +9 -5
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +317 -22
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +71 -14
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +75 -13
- package/docs/training.md +280 -32
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/package.json +12 -4
- package/src/api/PaddockSimulatorController.js +6 -33
- package/src/app/BrowserExpertAdapter.js +76 -9
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +291 -1630
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +4 -0
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +20 -0
- package/src/config/restartOptions.js +34 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +261 -0
- package/src/environment/episode.js +98 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +376 -10
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +199 -141
- package/src/environment/options.js +37 -5
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +272 -23
- package/src/environment/scenarios.js +57 -9
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +75 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +66 -0
- package/src/environment/sensors/surfaceRays.js +360 -0
- package/src/environment/sensors/trackRays.js +424 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +159 -26
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +171 -6
- package/src/index.js +6 -32
- package/src/rendering/proceduralTrackAsset.js +20 -823
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +1 -1
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -619
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +239 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +60 -0
- package/src/simulation/pit/pitRouteMovement.js +167 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +43 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +94 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +67 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +129 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +92 -0
- package/src/simulation/race/stalledDnf.js +77 -0
- package/src/simulation/raceSimulation.js +231 -2817
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +144 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/trackLimitsSteward.js +1 -1
- package/src/simulation/rulesConfig.js +5 -323
- package/src/simulation/snapshots/raceSnapshots.js +193 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +659 -0
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -1706
- package/src/simulation/vehicle/contactResolution.js +181 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +87 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +364 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
- package/src/simulation/vehicle/vehicleState.js +220 -0
- package/src/simulation/vehicle/wheelSurface.js +152 -0
- package/src/simulation/vehicleGeometry.js +1 -247
- package/src/simulation/vehiclePhysics.js +1 -136
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -338
- package/src/styles.css +42 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +14 -360
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +5 -4
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/timingTowerTemplate.js +34 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { buildCollisionCandidatePairs, detectVehicleCollision } from '../collisionGeometry.js';
|
|
2
|
+
import { canCollide, isCollidable } from '../participants/participantInteractions.js';
|
|
3
|
+
import { clamp, normalizeAngle } from '../simMath.js';
|
|
4
|
+
import { VEHICLE_LIMITS } from './vehiclePhysics.js';
|
|
5
|
+
import { shiftPreviousRenderPose } from '../pit/pitRouting.js';
|
|
6
|
+
|
|
7
|
+
const MAX_COLLISION_CORRECTION = 4.5;
|
|
8
|
+
|
|
9
|
+
function isPitPositionControlledCar(car) {
|
|
10
|
+
const status = car?.pitStop?.status;
|
|
11
|
+
return status === 'entering' || status === 'queued' || status === 'servicing' || status === 'exiting';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function forwardVector(car) {
|
|
15
|
+
return { x: Math.cos(car.heading), y: Math.sin(car.heading) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function velocityVector(car) {
|
|
19
|
+
if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
|
|
20
|
+
return { x: car.velocityX, y: car.velocityY };
|
|
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;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function applyVelocityVector(car, velocity) {
|
|
31
|
+
const speed = clamp(Math.hypot(velocity.x, velocity.y), 0, VEHICLE_LIMITS.maxSpeed);
|
|
32
|
+
if (speed <= 0) {
|
|
33
|
+
car.velocityX = 0;
|
|
34
|
+
car.velocityY = 0;
|
|
35
|
+
car.speed = 0;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const scale = speed / Math.max(1e-9, Math.hypot(velocity.x, velocity.y));
|
|
39
|
+
car.velocityX = velocity.x * scale;
|
|
40
|
+
car.velocityY = velocity.y * scale;
|
|
41
|
+
car.speed = speed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveCollisionsForSimulation(sim) {
|
|
45
|
+
const collidableCars = sim.cars.filter(isCollidable);
|
|
46
|
+
if (collidableCars.length < 2) return;
|
|
47
|
+
const reportedContacts = new Set();
|
|
48
|
+
|
|
49
|
+
for (let pass = 0; pass < 3; pass += 1) {
|
|
50
|
+
const candidates = buildCollisionCandidatePairs(collidableCars, { trackLength: sim.track.length });
|
|
51
|
+
for (const [first, second] of candidates) {
|
|
52
|
+
if (!canCollide(first, second)) continue;
|
|
53
|
+
const collision = detectVehicleCollision(first, second);
|
|
54
|
+
if (!collision) continue;
|
|
55
|
+
const firstPitControlled = isPitPositionControlledCar(first);
|
|
56
|
+
const secondPitControlled = isPitPositionControlledCar(second);
|
|
57
|
+
if (firstPitControlled && secondPitControlled) continue;
|
|
58
|
+
const stewardCollision = {
|
|
59
|
+
...collision,
|
|
60
|
+
trackLength: sim.track.length,
|
|
61
|
+
};
|
|
62
|
+
const oneCarFixed = firstPitControlled || secondPitControlled;
|
|
63
|
+
|
|
64
|
+
const correction = Math.min(
|
|
65
|
+
(oneCarFixed ? collision.depth : collision.depth / 2) + 0.65,
|
|
66
|
+
MAX_COLLISION_CORRECTION,
|
|
67
|
+
);
|
|
68
|
+
const firstCorrectionX = firstPitControlled ? 0 : -collision.axis.x * correction;
|
|
69
|
+
const firstCorrectionY = firstPitControlled ? 0 : -collision.axis.y * correction;
|
|
70
|
+
const secondCorrectionX = secondPitControlled ? 0 : collision.axis.x * correction;
|
|
71
|
+
const secondCorrectionY = secondPitControlled ? 0 : collision.axis.y * correction;
|
|
72
|
+
if (!firstPitControlled) {
|
|
73
|
+
first.x += firstCorrectionX;
|
|
74
|
+
first.y += firstCorrectionY;
|
|
75
|
+
}
|
|
76
|
+
if (!secondPitControlled) {
|
|
77
|
+
second.x += secondCorrectionX;
|
|
78
|
+
second.y += secondCorrectionY;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (oneCarFixed) {
|
|
82
|
+
if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode === 'simulator');
|
|
83
|
+
if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode === 'simulator');
|
|
84
|
+
} else {
|
|
85
|
+
applyContactVelocityResponse(sim, first, second, collision.axis);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const yawNudge = clamp(collision.depth * 0.0025, 0.008, 0.035);
|
|
89
|
+
const freshContact = first.contactCooldown <= 0 && second.contactCooldown <= 0;
|
|
90
|
+
const firstHeadingCorrection = firstPitControlled ? 0 : -collision.axis.y * yawNudge;
|
|
91
|
+
const secondHeadingCorrection = secondPitControlled ? 0 : collision.axis.y * yawNudge;
|
|
92
|
+
if (!firstPitControlled) {
|
|
93
|
+
first.heading = normalizeAngle(first.heading + firstHeadingCorrection);
|
|
94
|
+
shiftPreviousRenderPose(first, firstCorrectionX, firstCorrectionY, firstHeadingCorrection);
|
|
95
|
+
}
|
|
96
|
+
if (!secondPitControlled) {
|
|
97
|
+
second.heading = normalizeAngle(second.heading + secondHeadingCorrection);
|
|
98
|
+
shiftPreviousRenderPose(second, secondCorrectionX, secondCorrectionY, secondHeadingCorrection);
|
|
99
|
+
}
|
|
100
|
+
first.contactCooldown = 1;
|
|
101
|
+
second.contactCooldown = 1;
|
|
102
|
+
|
|
103
|
+
const contactKey = `${first.id}:${second.id}`;
|
|
104
|
+
if (freshContact && pass === 0 && !reportedContacts.has(contactKey)) {
|
|
105
|
+
reportedContacts.add(contactKey);
|
|
106
|
+
sim.events.unshift({
|
|
107
|
+
type: 'contact',
|
|
108
|
+
at: sim.time,
|
|
109
|
+
carId: first.id,
|
|
110
|
+
otherCarId: second.id,
|
|
111
|
+
firstShapeId: collision.firstShapeId,
|
|
112
|
+
secondShapeId: collision.secondShapeId,
|
|
113
|
+
contactType: collision.contactType,
|
|
114
|
+
depth: collision.depth,
|
|
115
|
+
timeOfImpact: collision.timeOfImpact,
|
|
116
|
+
});
|
|
117
|
+
sim.reviewCollision(first, second, stewardCollision);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function applyContactVelocityResponse(sim, first, second, axis) {
|
|
124
|
+
if (
|
|
125
|
+
sim.physicsMode === 'simulator' ||
|
|
126
|
+
Number.isFinite(first.velocityX) ||
|
|
127
|
+
Number.isFinite(second.velocityX)
|
|
128
|
+
) {
|
|
129
|
+
const firstVelocity = velocityVector(first);
|
|
130
|
+
const secondVelocity = velocityVector(second);
|
|
131
|
+
const relativeNormalVelocity = dot({
|
|
132
|
+
x: secondVelocity.x - firstVelocity.x,
|
|
133
|
+
y: secondVelocity.y - firstVelocity.y,
|
|
134
|
+
}, axis);
|
|
135
|
+
|
|
136
|
+
if (relativeNormalVelocity < 0) {
|
|
137
|
+
const impulse = clamp(-relativeNormalVelocity * (0.34 + sim.rules.collisionRestitution), 0, 16);
|
|
138
|
+
firstVelocity.x -= axis.x * impulse;
|
|
139
|
+
firstVelocity.y -= axis.y * impulse;
|
|
140
|
+
secondVelocity.x += axis.x * impulse;
|
|
141
|
+
secondVelocity.y += axis.y * impulse;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
applyVelocityVector(first, {
|
|
145
|
+
x: firstVelocity.x * 0.997,
|
|
146
|
+
y: firstVelocity.y * 0.997,
|
|
147
|
+
});
|
|
148
|
+
applyVelocityVector(second, {
|
|
149
|
+
x: secondVelocity.x * 0.997,
|
|
150
|
+
y: secondVelocity.y * 0.997,
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const firstForward = forwardVector(first);
|
|
156
|
+
const secondForward = forwardVector(second);
|
|
157
|
+
const firstNormal = dot(firstForward, axis);
|
|
158
|
+
const secondNormal = dot(secondForward, axis);
|
|
159
|
+
const relativeNormalVelocity = second.speed * secondNormal - first.speed * firstNormal;
|
|
160
|
+
|
|
161
|
+
if (relativeNormalVelocity < 0) {
|
|
162
|
+
const impulse = clamp(-relativeNormalVelocity * (0.34 + sim.rules.collisionRestitution), 0, 16);
|
|
163
|
+
if (firstNormal > 0) first.speed = clamp(first.speed - impulse * firstNormal, 0, VEHICLE_LIMITS.maxSpeed);
|
|
164
|
+
if (secondNormal < 0) second.speed = clamp(second.speed + impulse * secondNormal, 0, VEHICLE_LIMITS.maxSpeed);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
first.speed = clamp(first.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
168
|
+
second.speed = clamp(second.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function dampPitContactVelocity(car, factor, syncVelocity = false) {
|
|
172
|
+
if (syncVelocity || Number.isFinite(car.velocityX) || Number.isFinite(car.velocityY)) {
|
|
173
|
+
const velocity = velocityVector(car);
|
|
174
|
+
applyVelocityVector(car, {
|
|
175
|
+
x: velocity.x * factor,
|
|
176
|
+
y: velocity.y * factor,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
car.speed = clamp(car.speed * factor, 0, VEHICLE_LIMITS.maxSpeed);
|
|
181
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function mainTrackSurface(track, crossTrackError) {
|
|
2
|
+
const trackEdge = track.width / 2;
|
|
3
|
+
const kerbEdge = trackEdge + (track.kerbWidth ?? 0);
|
|
4
|
+
const gravelEdge = kerbEdge + track.gravelWidth;
|
|
5
|
+
const runoffEdge = gravelEdge + track.runoffWidth;
|
|
6
|
+
if (crossTrackError <= trackEdge) return 'track';
|
|
7
|
+
if (crossTrackError <= kerbEdge) return 'kerb';
|
|
8
|
+
if (crossTrackError <= gravelEdge) return 'gravel';
|
|
9
|
+
if (crossTrackError <= runoffEdge) return 'grass';
|
|
10
|
+
return 'barrier';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function wheelFullyOutside(samples, trackLimit) {
|
|
14
|
+
const rightOutside = samples.every((state) => !state.inPitLane && state.signedOffset > trackLimit);
|
|
15
|
+
const leftOutside = samples.every((state) => !state.inPitLane && state.signedOffset < -trackLimit);
|
|
16
|
+
return {
|
|
17
|
+
rightOutside,
|
|
18
|
+
leftOutside,
|
|
19
|
+
fullyOutsideWhiteLine: rightOutside || leftOutside,
|
|
20
|
+
outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit) {
|
|
25
|
+
const rightOutside = minimumSignedOffset > trackLimit;
|
|
26
|
+
const leftOutside = maximumSignedOffset < -trackLimit;
|
|
27
|
+
return {
|
|
28
|
+
rightOutside,
|
|
29
|
+
leftOutside,
|
|
30
|
+
fullyOutsideWhiteLine: rightOutside || leftOutside,
|
|
31
|
+
outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function analyticWheelState(patch, centerState, track, trackLimit) {
|
|
36
|
+
const wheelCenterOffset =
|
|
37
|
+
(patch.center.x - centerState.x) * centerState.normalX +
|
|
38
|
+
(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
|
+
const minimumSignedOffset = wheelCenterOffset - projectedHalfWidth;
|
|
43
|
+
const maximumSignedOffset = wheelCenterOffset + projectedHalfWidth;
|
|
44
|
+
const signedOffset = Math.abs(minimumSignedOffset) > Math.abs(maximumSignedOffset)
|
|
45
|
+
? minimumSignedOffset
|
|
46
|
+
: maximumSignedOffset;
|
|
47
|
+
const state = stateFromSignedOffset(centerState, track, signedOffset);
|
|
48
|
+
const outside = wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
id: patch.id,
|
|
52
|
+
x: patch.center.x,
|
|
53
|
+
y: patch.center.y,
|
|
54
|
+
signedOffset: state.signedOffset,
|
|
55
|
+
crossTrackError: state.crossTrackError,
|
|
56
|
+
surface: state.surface,
|
|
57
|
+
onTrack: Boolean(state.onTrack),
|
|
58
|
+
inPitLane: false,
|
|
59
|
+
pitLanePart: null,
|
|
60
|
+
pitBoxId: null,
|
|
61
|
+
minimumSignedOffset,
|
|
62
|
+
maximumSignedOffset,
|
|
63
|
+
fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
|
|
64
|
+
outsideSide: outside.outsideSide,
|
|
65
|
+
sampledStates: [state],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stateFromSignedOffset(centerState, track, signedOffset) {
|
|
70
|
+
const crossTrackError = Math.abs(signedOffset);
|
|
71
|
+
const surface = mainTrackSurface(track, crossTrackError);
|
|
72
|
+
return {
|
|
73
|
+
...centerState,
|
|
74
|
+
signedOffset,
|
|
75
|
+
crossTrackError,
|
|
76
|
+
surface,
|
|
77
|
+
onTrack: surface === 'track' || surface === 'kerb',
|
|
78
|
+
inPitLane: false,
|
|
79
|
+
pitLanePart: null,
|
|
80
|
+
pitBoxId: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { metersToSimUnits } from '../units.js';
|
|
2
|
+
|
|
3
|
+
const PIT_CONNECTOR_FULL_SAMPLE_WINDOW = metersToSimUnits(35);
|
|
4
|
+
|
|
5
|
+
export function patchSamples(patch) {
|
|
6
|
+
return [patch.center, ...patch.corners];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isNearPitConnector(track, centerState) {
|
|
10
|
+
const pitLane = track.pitLane;
|
|
11
|
+
if (!pitLane?.enabled) return false;
|
|
12
|
+
const distance = centerState.distance;
|
|
13
|
+
const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
|
|
14
|
+
const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
|
|
15
|
+
return wrappedDistanceDelta(distance, entryDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW ||
|
|
16
|
+
wrappedDistanceDelta(distance, exitDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function canUseAnalyticPitWheels(geometry, centerState) {
|
|
20
|
+
const roadWidth = Number(centerState?.pitLaneRoadWidth);
|
|
21
|
+
if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
|
|
22
|
+
const halfRoadWidth = roadWidth / 2;
|
|
23
|
+
return geometry.contactPatches.every((patch) => {
|
|
24
|
+
const range = patchPitOffsetRange(patch, centerState);
|
|
25
|
+
return range.minimum >= -halfRoadWidth - 0.001 && range.maximum <= halfRoadWidth + 0.001;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function analyticPitWheelState(patch, centerState) {
|
|
30
|
+
const range = patchPitOffsetRange(patch, centerState);
|
|
31
|
+
return {
|
|
32
|
+
id: patch.id,
|
|
33
|
+
x: patch.center.x,
|
|
34
|
+
y: patch.center.y,
|
|
35
|
+
signedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
|
|
36
|
+
crossTrackError: centerState.mainTrackCrossTrackError ?? centerState.crossTrackError,
|
|
37
|
+
surface: centerState.surface,
|
|
38
|
+
onTrack: true,
|
|
39
|
+
inPitLane: true,
|
|
40
|
+
pitLanePart: centerState.pitLanePart ?? null,
|
|
41
|
+
pitBoxId: centerState.pitBoxId ?? null,
|
|
42
|
+
minimumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
|
|
43
|
+
maximumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
|
|
44
|
+
pitLaneMinimumSignedOffset: range.minimum,
|
|
45
|
+
pitLaneMaximumSignedOffset: range.maximum,
|
|
46
|
+
fullyOutsideWhiteLine: false,
|
|
47
|
+
outsideSide: 0,
|
|
48
|
+
sampledStates: [centerState],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function patchPitOffsetRange(patch, centerState) {
|
|
53
|
+
const wheelCenterOffset =
|
|
54
|
+
(patch.center.x - centerState.x) * centerState.normalX +
|
|
55
|
+
(patch.center.y - centerState.y) * centerState.normalY;
|
|
56
|
+
const projectedHalfWidth =
|
|
57
|
+
Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
|
|
58
|
+
Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
|
|
59
|
+
return {
|
|
60
|
+
minimum: wheelCenterOffset - projectedHalfWidth,
|
|
61
|
+
maximum: wheelCenterOffset + projectedHalfWidth,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function wrappedDistanceDelta(first, second, length) {
|
|
66
|
+
if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(length) || length <= 0) return Infinity;
|
|
67
|
+
const delta = Math.abs(first - second);
|
|
68
|
+
return Math.min(delta, length - delta);
|
|
69
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { VEHICLE_GEOMETRY, getVehicleGeometryState, vehicleAxes } from './vehicleGeometry.js';
|
|
2
|
+
import { applyWheelSurfaceState } from './wheelSurface.js';
|
|
3
|
+
import { freezeVehicleMotion } from './vehicleKinematics.js';
|
|
4
|
+
import { nearestTrackStateForCar, pitOverrideAllowedForCar } from '../track/trackStatePolicy.js';
|
|
5
|
+
import { markCarDnf } from '../race/retirements.js';
|
|
6
|
+
|
|
7
|
+
export function applyRunoffResponseForSimulation(sim, car) {
|
|
8
|
+
if (car.destroyed) {
|
|
9
|
+
freezeBarrierMotion(car, sim.physicsMode);
|
|
10
|
+
applyWheelSurfaceState(car, sim.track);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const allowPitOverride = pitOverrideAllowedForCar(car);
|
|
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;
|
|
18
|
+
const barrierCenter = sim.track.width / 2 + (sim.track.kerbWidth ?? 0) + sim.track.gravelWidth + sim.track.runoffWidth;
|
|
19
|
+
const signedLimit = barrierCenter - (sim.track.barrierWidth ?? 0) / 2;
|
|
20
|
+
const side = Math.sign(mainTrackState.signedOffset) || 1;
|
|
21
|
+
const outwardReach = getOutwardVehicleReach(car, mainTrackState, side);
|
|
22
|
+
const overshoot = Math.abs(mainTrackState.signedOffset) + outwardReach - signedLimit;
|
|
23
|
+
if (overshoot <= 0) {
|
|
24
|
+
if (state.inPitLane) {
|
|
25
|
+
applyWheelSurfaceState(car, sim.track, { centerState: state });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
applyWheelSurfaceState(car, sim.track, { centerState: state });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
destroyCarOnBarrier(sim, car);
|
|
33
|
+
applyWheelSurfaceState(car, sim.track, { centerState: mainTrackState });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getOutwardVehicleReach(car, state, side) {
|
|
37
|
+
const visualReach = getVisualFootprintOutwardReach(car, state);
|
|
38
|
+
const geometry = getVehicleGeometryState(car);
|
|
39
|
+
let reach = visualReach;
|
|
40
|
+
geometry.current.shapes.forEach((shape) => {
|
|
41
|
+
shape.corners.forEach((corner) => {
|
|
42
|
+
const lateral =
|
|
43
|
+
(corner.x - car.x) * state.normalX +
|
|
44
|
+
(corner.y - car.y) * state.normalY;
|
|
45
|
+
reach = Math.max(reach, lateral * side);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return reach;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getVisualFootprintOutwardReach(car, state) {
|
|
52
|
+
const axes = vehicleAxes(car.heading ?? 0);
|
|
53
|
+
return (
|
|
54
|
+
Math.abs(axes.forward.x * state.normalX + axes.forward.y * state.normalY) * VEHICLE_GEOMETRY.visualLength / 2 +
|
|
55
|
+
Math.abs(axes.right.x * state.normalX + axes.right.y * state.normalY) * VEHICLE_GEOMETRY.visualWidth / 2
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function destroyCarOnBarrier(sim, car) {
|
|
60
|
+
if (car.destroyed || car.finished) return;
|
|
61
|
+
car.destroyed = true;
|
|
62
|
+
car.destroyReason = 'barrier';
|
|
63
|
+
car.destroyedAt = sim.time;
|
|
64
|
+
car.outOfRace = true;
|
|
65
|
+
markCarDnf(sim, car, { reason: 'barrier' });
|
|
66
|
+
car.canAttack = false;
|
|
67
|
+
car.drsActive = false;
|
|
68
|
+
car.drsEligible = false;
|
|
69
|
+
car.drsZoneId = null;
|
|
70
|
+
car.drsZoneEnabled = false;
|
|
71
|
+
freezeBarrierMotion(car, sim.physicsMode);
|
|
72
|
+
sim.events.unshift({
|
|
73
|
+
type: 'car-destroyed',
|
|
74
|
+
at: sim.time,
|
|
75
|
+
carId: car.id,
|
|
76
|
+
driverId: car.id,
|
|
77
|
+
reason: 'barrier',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function freezeBarrierMotion(car, physicsMode) {
|
|
82
|
+
freezeVehicleMotion(car, {
|
|
83
|
+
clearManualControls: true,
|
|
84
|
+
physicsMode,
|
|
85
|
+
stabilityState: 'destroyed',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const SURFACE_PRIORITY = {
|
|
2
|
+
track: 0,
|
|
3
|
+
'pit-entry': 1,
|
|
4
|
+
'pit-exit': 1,
|
|
5
|
+
'pit-lane': 2,
|
|
6
|
+
'pit-box': 3,
|
|
7
|
+
kerb: 4,
|
|
8
|
+
grass: 5,
|
|
9
|
+
gravel: 6,
|
|
10
|
+
barrier: 7,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function priority(surface) {
|
|
14
|
+
return SURFACE_PRIORITY[surface] ?? SURFACE_PRIORITY.barrier;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function worstState(states) {
|
|
18
|
+
return states.reduce((worst, state) => (
|
|
19
|
+
priority(state.surface) > priority(worst.surface) ? state : worst
|
|
20
|
+
), states[0]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getEffectiveSurface(wheels = []) {
|
|
24
|
+
if (!wheels.length) return 'track';
|
|
25
|
+
return wheels.reduce((surface, wheel) => (
|
|
26
|
+
priority(wheel.surface) > priority(surface) ? wheel.surface : surface
|
|
27
|
+
), 'track');
|
|
28
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { normalizeAngle } from '../simMath.js';
|
|
2
|
+
import {
|
|
3
|
+
REAL_F1_CAR_LENGTH_METERS,
|
|
4
|
+
REAL_F1_CAR_WIDTH_METERS,
|
|
5
|
+
REAL_F1_WHEELBASE_METERS,
|
|
6
|
+
metersToSimUnits,
|
|
7
|
+
} from '../units.js';
|
|
8
|
+
|
|
9
|
+
export const VEHICLE_GEOMETRY = {
|
|
10
|
+
visualLength: metersToSimUnits(REAL_F1_CAR_LENGTH_METERS),
|
|
11
|
+
visualWidth: metersToSimUnits(REAL_F1_CAR_WIDTH_METERS),
|
|
12
|
+
bodyLength: metersToSimUnits(4.65),
|
|
13
|
+
bodyWidth: metersToSimUnits(1.24),
|
|
14
|
+
wheelLength: metersToSimUnits(0.72),
|
|
15
|
+
wheelWidth: metersToSimUnits(0.38),
|
|
16
|
+
wheelLongitudinalOffset: metersToSimUnits(REAL_F1_WHEELBASE_METERS / 2),
|
|
17
|
+
wheelLateralOffset: metersToSimUnits((REAL_F1_CAR_WIDTH_METERS - 0.38) / 2),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const WHEEL_SPECS = [
|
|
21
|
+
{ id: 'front-left', longitudinal: 1, lateral: -1 },
|
|
22
|
+
{ id: 'front-right', longitudinal: 1, lateral: 1 },
|
|
23
|
+
{ id: 'rear-left', longitudinal: -1, lateral: -1 },
|
|
24
|
+
{ id: 'rear-right', longitudinal: -1, lateral: 1 },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function finiteOr(value, fallback) {
|
|
28
|
+
return Number.isFinite(value) ? value : fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function vehiclePose(car, { previous = false } = {}) {
|
|
32
|
+
const x = previous ? finiteOr(car.previousX, car.x) : car.x;
|
|
33
|
+
const y = previous ? finiteOr(car.previousY, car.y) : car.y;
|
|
34
|
+
const heading = previous ? finiteOr(car.previousHeading, car.heading) : car.heading;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
x: finiteOr(x, 0),
|
|
38
|
+
y: finiteOr(y, 0),
|
|
39
|
+
heading: normalizeAngle(finiteOr(heading, 0)),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function vehicleAxes(heading) {
|
|
44
|
+
const cos = Math.cos(heading);
|
|
45
|
+
const sin = Math.sin(heading);
|
|
46
|
+
return {
|
|
47
|
+
forward: { x: cos, y: sin },
|
|
48
|
+
right: { x: -sin, y: cos },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function offsetPoint(center, axes, longitudinal, lateral) {
|
|
53
|
+
return {
|
|
54
|
+
x: center.x + axes.forward.x * longitudinal + axes.right.x * lateral,
|
|
55
|
+
y: center.y + axes.forward.y * longitudinal + axes.right.y * lateral,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createOrientedRect({ id, type, center, heading, length, width }) {
|
|
60
|
+
const axes = vehicleAxes(heading);
|
|
61
|
+
const halfLength = length / 2;
|
|
62
|
+
const halfWidth = width / 2;
|
|
63
|
+
const corners = [
|
|
64
|
+
offsetPoint(center, axes, halfLength, halfWidth),
|
|
65
|
+
offsetPoint(center, axes, halfLength, -halfWidth),
|
|
66
|
+
offsetPoint(center, axes, -halfLength, -halfWidth),
|
|
67
|
+
offsetPoint(center, axes, -halfLength, halfWidth),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
type,
|
|
73
|
+
center,
|
|
74
|
+
heading,
|
|
75
|
+
length,
|
|
76
|
+
width,
|
|
77
|
+
halfLength,
|
|
78
|
+
halfWidth,
|
|
79
|
+
forward: axes.forward,
|
|
80
|
+
right: axes.right,
|
|
81
|
+
corners,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createVehicleGeometry(car, options = {}) {
|
|
86
|
+
const pose = vehiclePose(car, options);
|
|
87
|
+
const axes = vehicleAxes(pose.heading);
|
|
88
|
+
const center = { x: pose.x, y: pose.y };
|
|
89
|
+
const body = createOrientedRect({
|
|
90
|
+
id: 'body',
|
|
91
|
+
type: 'body',
|
|
92
|
+
center,
|
|
93
|
+
heading: pose.heading,
|
|
94
|
+
length: VEHICLE_GEOMETRY.bodyLength,
|
|
95
|
+
width: VEHICLE_GEOMETRY.bodyWidth,
|
|
96
|
+
});
|
|
97
|
+
const wheels = WHEEL_SPECS.map((spec) => {
|
|
98
|
+
const wheelCenter = offsetPoint(
|
|
99
|
+
center,
|
|
100
|
+
axes,
|
|
101
|
+
spec.longitudinal * VEHICLE_GEOMETRY.wheelLongitudinalOffset,
|
|
102
|
+
spec.lateral * VEHICLE_GEOMETRY.wheelLateralOffset,
|
|
103
|
+
);
|
|
104
|
+
return createOrientedRect({
|
|
105
|
+
id: spec.id,
|
|
106
|
+
type: 'wheel',
|
|
107
|
+
center: wheelCenter,
|
|
108
|
+
heading: pose.heading,
|
|
109
|
+
length: VEHICLE_GEOMETRY.wheelLength,
|
|
110
|
+
width: VEHICLE_GEOMETRY.wheelWidth,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
body,
|
|
116
|
+
wheels,
|
|
117
|
+
contactPatches: wheels,
|
|
118
|
+
shapes: [body, ...wheels],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function geometryPoseSignature(pose) {
|
|
123
|
+
return [
|
|
124
|
+
pose.x,
|
|
125
|
+
pose.y,
|
|
126
|
+
pose.heading,
|
|
127
|
+
pose.previousX,
|
|
128
|
+
pose.previousY,
|
|
129
|
+
pose.previousHeading,
|
|
130
|
+
].join(':');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function geometryPose(car) {
|
|
134
|
+
const x = finiteOr(car.x, 0);
|
|
135
|
+
const y = finiteOr(car.y, 0);
|
|
136
|
+
const heading = normalizeAngle(finiteOr(car.heading, 0));
|
|
137
|
+
return {
|
|
138
|
+
x,
|
|
139
|
+
y,
|
|
140
|
+
heading,
|
|
141
|
+
previousX: finiteOr(car.previousX, x),
|
|
142
|
+
previousY: finiteOr(car.previousY, y),
|
|
143
|
+
previousHeading: normalizeAngle(finiteOr(car.previousHeading, heading)),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function geometryStateMatches(car, state) {
|
|
148
|
+
const pose = state?.pose;
|
|
149
|
+
if (!pose) return false;
|
|
150
|
+
const x = finiteOr(car.x, 0);
|
|
151
|
+
const y = finiteOr(car.y, 0);
|
|
152
|
+
const heading = normalizeAngle(finiteOr(car.heading, 0));
|
|
153
|
+
return (
|
|
154
|
+
pose.x === x &&
|
|
155
|
+
pose.y === y &&
|
|
156
|
+
pose.heading === heading &&
|
|
157
|
+
pose.previousX === finiteOr(car.previousX, x) &&
|
|
158
|
+
pose.previousY === finiteOr(car.previousY, y) &&
|
|
159
|
+
pose.previousHeading === normalizeAngle(finiteOr(car.previousHeading, heading))
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function createVehicleShapeAabb(shape) {
|
|
164
|
+
let minX = Infinity;
|
|
165
|
+
let maxX = -Infinity;
|
|
166
|
+
let minY = Infinity;
|
|
167
|
+
let maxY = -Infinity;
|
|
168
|
+
for (let index = 0; index < shape.corners.length; index += 1) {
|
|
169
|
+
const corner = shape.corners[index];
|
|
170
|
+
if (corner.x < minX) minX = corner.x;
|
|
171
|
+
if (corner.x > maxX) maxX = corner.x;
|
|
172
|
+
if (corner.y < minY) minY = corner.y;
|
|
173
|
+
if (corner.y > maxY) maxY = corner.y;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
minX,
|
|
177
|
+
maxX,
|
|
178
|
+
minY,
|
|
179
|
+
maxY,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function mergeAabbs(aabbs) {
|
|
184
|
+
let minX = Infinity;
|
|
185
|
+
let maxX = -Infinity;
|
|
186
|
+
let minY = Infinity;
|
|
187
|
+
let maxY = -Infinity;
|
|
188
|
+
for (let index = 0; index < aabbs.length; index += 1) {
|
|
189
|
+
const aabb = aabbs[index];
|
|
190
|
+
if (aabb.minX < minX) minX = aabb.minX;
|
|
191
|
+
if (aabb.maxX > maxX) maxX = aabb.maxX;
|
|
192
|
+
if (aabb.minY < minY) minY = aabb.minY;
|
|
193
|
+
if (aabb.maxY > maxY) maxY = aabb.maxY;
|
|
194
|
+
}
|
|
195
|
+
return { minX, maxX, minY, maxY };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function createVehicleGeometryState(car) {
|
|
199
|
+
const pose = geometryPose(car);
|
|
200
|
+
const current = createVehicleGeometry(car);
|
|
201
|
+
const previous = createVehicleGeometry(car, { previous: true });
|
|
202
|
+
const bodyAabb = createVehicleShapeAabb(current.body);
|
|
203
|
+
const previousBodyAabb = createVehicleShapeAabb(previous.body);
|
|
204
|
+
const sweptBodyAabb = mergeAabbs([previousBodyAabb, bodyAabb]);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
pose,
|
|
208
|
+
signature: geometryPoseSignature(pose),
|
|
209
|
+
current,
|
|
210
|
+
previous,
|
|
211
|
+
body: current.body,
|
|
212
|
+
wheels: current.wheels,
|
|
213
|
+
contactPatches: current.contactPatches,
|
|
214
|
+
bodyAabb,
|
|
215
|
+
previousBodyAabb,
|
|
216
|
+
sweptBodyAabb,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function getVehicleGeometryState(car) {
|
|
221
|
+
if (geometryStateMatches(car, car.geometryState)) return car.geometryState;
|
|
222
|
+
car.geometryState = createVehicleGeometryState(car);
|
|
223
|
+
return car.geometryState;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function createVehicleAabb(car, options = {}) {
|
|
227
|
+
const geometry = createVehicleGeometry(car, options);
|
|
228
|
+
return mergeAabbs(geometry.shapes.map(createVehicleShapeAabb));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function interpolateVehiclePose(car, amount) {
|
|
232
|
+
const previousX = finiteOr(car.previousX, car.x);
|
|
233
|
+
const previousY = finiteOr(car.previousY, car.y);
|
|
234
|
+
const previousHeading = finiteOr(car.previousHeading, car.heading);
|
|
235
|
+
const headingDelta = normalizeAngle(finiteOr(car.heading, 0) - previousHeading);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
...car,
|
|
239
|
+
x: previousX + (finiteOr(car.x, previousX) - previousX) * amount,
|
|
240
|
+
y: previousY + (finiteOr(car.y, previousY) - previousY) * amount,
|
|
241
|
+
heading: normalizeAngle(previousHeading + headingDelta * amount),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function getCarCorners(car) {
|
|
246
|
+
return createVehicleGeometry(car).body.corners;
|
|
247
|
+
}
|