@inventure71/paddockjs 1.0.0 → 2.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 +52 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +194 -12
- package/docs/architecture.md +119 -44
- package/docs/concepts.md +4 -2
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +299 -17
- package/docs/index.md +1 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +37 -10
- package/docs/system_specs.md +23 -10
- package/docs/training.md +272 -30
- 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 +4 -0
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +305 -1603
- 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/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +361 -9
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observations.js +325 -40
- package/src/environment/options.js +37 -5
- package/src/environment/runtime.js +222 -21
- package/src/environment/scenarios.js +37 -8
- 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 +65 -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 +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +100 -5
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +169 -6
- package/src/index.js +5 -0
- 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 +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -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 +92 -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 +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -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 +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +230 -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 +141 -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 +192 -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/trackQueryIndex.js +945 -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 +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -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
|
@@ -1,136 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
REAL_F1_WHEELBASE_METERS,
|
|
4
|
-
SIM_UNITS_PER_METER,
|
|
5
|
-
TOP_SPEED_SIM_UNITS_PER_SECOND,
|
|
6
|
-
simUnitsToMeters,
|
|
7
|
-
metersToSimUnits,
|
|
8
|
-
} from './units.js';
|
|
9
|
-
import { VEHICLE_GEOMETRY, getCarCorners } from './vehicleGeometry.js';
|
|
10
|
-
|
|
11
|
-
const G = 9.80665;
|
|
12
|
-
|
|
13
|
-
export const VEHICLE_LIMITS = {
|
|
14
|
-
wheelbase: metersToSimUnits(REAL_F1_WHEELBASE_METERS),
|
|
15
|
-
maxSteer: 0.56,
|
|
16
|
-
steerRate: 2.35,
|
|
17
|
-
maxSpeed: TOP_SPEED_SIM_UNITS_PER_SECOND,
|
|
18
|
-
carLength: VEHICLE_GEOMETRY.visualLength,
|
|
19
|
-
carWidth: VEHICLE_GEOMETRY.visualWidth,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const SURFACE_MODEL = {
|
|
23
|
-
track: { grip: 1, drag: 0, rollingResistance: 0 },
|
|
24
|
-
'pit-entry': { grip: 0.96, drag: 0.04, rollingResistance: 0.012 },
|
|
25
|
-
'pit-lane': { grip: 0.96, drag: 0.04, rollingResistance: 0.012 },
|
|
26
|
-
'pit-exit': { grip: 0.96, drag: 0.04, rollingResistance: 0.012 },
|
|
27
|
-
'pit-box': { grip: 0.94, drag: 0.08, rollingResistance: 0.025 },
|
|
28
|
-
kerb: { grip: 0.92, drag: 0.12, rollingResistance: 0.045 },
|
|
29
|
-
gravel: { grip: 0.43, drag: 4.0, rollingResistance: 0.68 },
|
|
30
|
-
grass: { grip: 0.34, drag: 1.9, rollingResistance: 0.36 },
|
|
31
|
-
barrier: { grip: 0.18, drag: 9, rollingResistance: 1.2 },
|
|
32
|
-
};
|
|
33
|
-
const WHEEL_DRAG_YAW_GAIN = 0.08;
|
|
34
|
-
const MAX_WHEEL_DRAG_YAW_RATE = 0.22;
|
|
35
|
-
|
|
36
|
-
function accelerationLimit(speed, surfaceGrip) {
|
|
37
|
-
const speedRatio = clamp(speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
|
|
38
|
-
return SIM_UNITS_PER_METER * surfaceGrip * (17 * (1 - speedRatio ** 2.1) + 1.2);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function brakingLimit(speed, surfaceGrip) {
|
|
42
|
-
const speedRatio = clamp(speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
|
|
43
|
-
return SIM_UNITS_PER_METER * surfaceGrip * (20 + speedRatio * 14);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function tirePerformanceFactor(tireEnergy = 100) {
|
|
47
|
-
const normalized = clamp((Number(tireEnergy) || 1) / 100, 0.01, 1);
|
|
48
|
-
return clamp(0.45 + 0.55 * normalized ** 0.72, 0.45, 1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function surfaceResistance(surfaceName) {
|
|
52
|
-
const surface = SURFACE_MODEL[surfaceName] ?? SURFACE_MODEL.track;
|
|
53
|
-
return surface.drag * 0.14 + surface.rollingResistance * 2.4 + (1 - surface.grip) * 0.65;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function sideWheelResistance(wheels, side) {
|
|
57
|
-
const sideWheels = wheels.filter((wheel) => wheel.id?.endsWith(`-${side}`));
|
|
58
|
-
if (!sideWheels.length) return 0;
|
|
59
|
-
return sideWheels.reduce((total, wheel) => total + surfaceResistance(wheel.surface), 0) / sideWheels.length;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function wheelDragYawRate(car) {
|
|
63
|
-
const wheels = Array.isArray(car.wheelStates) ? car.wheelStates : [];
|
|
64
|
-
if (!wheels.length) return 0;
|
|
65
|
-
const leftResistance = sideWheelResistance(wheels, 'left');
|
|
66
|
-
const rightResistance = sideWheelResistance(wheels, 'right');
|
|
67
|
-
const speedFactor = clamp(car.speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
|
|
68
|
-
return clamp(
|
|
69
|
-
(rightResistance - leftResistance) * speedFactor * WHEEL_DRAG_YAW_GAIN,
|
|
70
|
-
-MAX_WHEEL_DRAG_YAW_RATE,
|
|
71
|
-
MAX_WHEEL_DRAG_YAW_RATE,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function integrateVehiclePhysics(car, controls, dt) {
|
|
76
|
-
const steeringTarget = clamp(controls.steering ?? 0, -VEHICLE_LIMITS.maxSteer, VEHICLE_LIMITS.maxSteer);
|
|
77
|
-
const steerDelta = clamp(
|
|
78
|
-
steeringTarget - car.steeringAngle,
|
|
79
|
-
-VEHICLE_LIMITS.steerRate * dt,
|
|
80
|
-
VEHICLE_LIMITS.steerRate * dt,
|
|
81
|
-
);
|
|
82
|
-
car.steeringAngle += steerDelta;
|
|
83
|
-
|
|
84
|
-
const throttle = clamp(controls.throttle ?? 0, 0, 1);
|
|
85
|
-
const brake = clamp(controls.brake ?? 0, 0, 1);
|
|
86
|
-
const surface = SURFACE_MODEL[car.trackState?.surface] ?? SURFACE_MODEL.track;
|
|
87
|
-
const tireFactor = tirePerformanceFactor(car.tireEnergy ?? 100);
|
|
88
|
-
const surfaceGrip = surface.grip * tireFactor;
|
|
89
|
-
const dragMultiplier = car.drsActive ? 0.42 : 1;
|
|
90
|
-
const speedRatio = clamp(car.speed / VEHICLE_LIMITS.maxSpeed, 0, 1);
|
|
91
|
-
const engineForce = throttle *
|
|
92
|
-
car.powerNewtons *
|
|
93
|
-
(car.drsActive ? 1.08 : 1) *
|
|
94
|
-
Math.max(0.12, 1 - speedRatio ** 1.15);
|
|
95
|
-
const brakeForce = brake * car.brakeNewtons;
|
|
96
|
-
const speedMetersPerSecondBefore = simUnitsToMeters(car.speed);
|
|
97
|
-
const dragForce = (car.dragCoefficient * dragMultiplier * 0.12 + surface.drag) *
|
|
98
|
-
speedMetersPerSecondBefore * speedMetersPerSecondBefore;
|
|
99
|
-
const rollingForce = surface.rollingResistance * car.mass * G;
|
|
100
|
-
const driveAcceleration = clamp((engineForce / car.mass) * SIM_UNITS_PER_METER, 0, accelerationLimit(car.speed, surfaceGrip));
|
|
101
|
-
const drsAcceleration = car.drsActive ? driveAcceleration * 0.06 : 0;
|
|
102
|
-
const brakeDeceleration = clamp((brakeForce / car.mass) * SIM_UNITS_PER_METER, 0, brakingLimit(car.speed, surfaceGrip));
|
|
103
|
-
const dragDeceleration = ((dragForce + rollingForce) / car.mass) * SIM_UNITS_PER_METER;
|
|
104
|
-
const acceleration = driveAcceleration + drsAcceleration - brakeDeceleration - dragDeceleration;
|
|
105
|
-
|
|
106
|
-
car.speed = clamp(car.speed + acceleration * dt, 0, VEHICLE_LIMITS.maxSpeed);
|
|
107
|
-
|
|
108
|
-
const speedMetersPerSecond = simUnitsToMeters(car.speed);
|
|
109
|
-
const rawYawRate = (speedMetersPerSecond / REAL_F1_WHEELBASE_METERS) *
|
|
110
|
-
Math.tan(car.steeringAngle) *
|
|
111
|
-
tireFactor;
|
|
112
|
-
const downforceGrip = car.downforceCoefficient * speedMetersPerSecond * speedMetersPerSecond / car.mass;
|
|
113
|
-
const tyreConditionGrip = tireFactor;
|
|
114
|
-
const maxYawRate = ((car.tireGrip * tyreConditionGrip * G + downforceGrip) * surfaceGrip) /
|
|
115
|
-
Math.max(speedMetersPerSecond, 1);
|
|
116
|
-
const steeringYawRate = clamp(rawYawRate, -maxYawRate, maxYawRate);
|
|
117
|
-
const requestedWheelDragYawRate = wheelDragYawRate(car);
|
|
118
|
-
car.yawRate = clamp(steeringYawRate + requestedWheelDragYawRate, -maxYawRate, maxYawRate);
|
|
119
|
-
car.wheelDragYawRate = car.yawRate - steeringYawRate;
|
|
120
|
-
car.turnRadius = Math.abs(car.yawRate) < 0.001 ? Infinity : car.speed / Math.abs(car.yawRate);
|
|
121
|
-
car.heading = normalizeAngle(car.heading + car.yawRate * dt);
|
|
122
|
-
car.x += Math.cos(car.heading) * car.speed * dt;
|
|
123
|
-
car.y += Math.sin(car.heading) * car.speed * dt;
|
|
124
|
-
car.throttle = throttle;
|
|
125
|
-
car.brake = brake;
|
|
126
|
-
car.lateralAcceleration = speedMetersPerSecond * car.yawRate;
|
|
127
|
-
car.steerSaturation = rawYawRate === 0 ? 0 : Math.abs(steeringYawRate / rawYawRate);
|
|
128
|
-
const tyreLoad = Math.abs(car.lateralAcceleration) / G;
|
|
129
|
-
const tireCare = clamp(Number(car.tireCare) || 1, 0.45, 1.8);
|
|
130
|
-
const wearRate = (0.035 + tyreLoad * 0.11 + brake * 0.07 + throttle * 0.025) / tireCare;
|
|
131
|
-
car.tireEnergy = clamp((car.tireEnergy ?? 100) - wearRate * dt, 1, 100);
|
|
132
|
-
|
|
133
|
-
return car;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export { getCarCorners };
|
|
1
|
+
export * from './vehicle/vehiclePhysics.js';
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const DEFAULT_WARMUP_POLICY = 'config-change';
|
|
2
|
+
const DEFAULT_WARMUP_STEPS = Object.freeze({
|
|
3
|
+
simulation: 20,
|
|
4
|
+
browser: 20,
|
|
5
|
+
environment: 20,
|
|
6
|
+
});
|
|
7
|
+
const SUPPORTED_WARMUP_POLICIES = new Set(['config-change', 'always', 'never']);
|
|
8
|
+
const MAX_CACHE_ENTRIES_PER_SURFACE = 256;
|
|
9
|
+
const warmupFingerprintCache = new Map();
|
|
10
|
+
|
|
11
|
+
export function normalizeWarmupOptions(input, surface = 'simulation') {
|
|
12
|
+
const fallbackSteps = DEFAULT_WARMUP_STEPS[surface] ?? DEFAULT_WARMUP_STEPS.simulation;
|
|
13
|
+
if (input === false) {
|
|
14
|
+
return {
|
|
15
|
+
enabled: false,
|
|
16
|
+
policy: 'never',
|
|
17
|
+
steps: fallbackSteps,
|
|
18
|
+
surface,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (input === true || input == null) {
|
|
22
|
+
return {
|
|
23
|
+
enabled: true,
|
|
24
|
+
policy: DEFAULT_WARMUP_POLICY,
|
|
25
|
+
steps: fallbackSteps,
|
|
26
|
+
surface,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const enabled = input.enabled == null ? true : Boolean(input.enabled);
|
|
30
|
+
const policy = normalizeWarmupPolicy(input.policy, enabled);
|
|
31
|
+
return {
|
|
32
|
+
enabled,
|
|
33
|
+
policy,
|
|
34
|
+
steps: normalizeWarmupSteps(input.steps, fallbackSteps),
|
|
35
|
+
surface: typeof input.surface === 'string' && input.surface.length > 0 ? input.surface : surface,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function withWarmupSurfaceOptions(options = {}, surface = 'simulation') {
|
|
40
|
+
return {
|
|
41
|
+
...options,
|
|
42
|
+
warmup: normalizeWarmupOptions(options.warmup, surface),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function disableWarmupOptions(options = {}, surface = 'simulation') {
|
|
47
|
+
const warmup = normalizeWarmupOptions(options.warmup, surface);
|
|
48
|
+
return {
|
|
49
|
+
...options,
|
|
50
|
+
warmup: {
|
|
51
|
+
...warmup,
|
|
52
|
+
enabled: false,
|
|
53
|
+
policy: 'never',
|
|
54
|
+
surface,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function runWarmupWithGuard({
|
|
60
|
+
options = {},
|
|
61
|
+
surface = 'simulation',
|
|
62
|
+
execute,
|
|
63
|
+
} = {}) {
|
|
64
|
+
const warmup = normalizeWarmupOptions(options.warmup, surface);
|
|
65
|
+
const fingerprint = createWarmupFingerprint(options, {
|
|
66
|
+
surface: warmup.surface,
|
|
67
|
+
steps: warmup.steps,
|
|
68
|
+
});
|
|
69
|
+
if (!shouldRunWarmup(warmup, fingerprint)) {
|
|
70
|
+
return { ran: false, warmup, fingerprint };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
execute?.({ warmup, fingerprint });
|
|
74
|
+
markWarmupComplete(warmup.surface, fingerprint);
|
|
75
|
+
return { ran: true, warmup, fingerprint };
|
|
76
|
+
} catch {
|
|
77
|
+
return { ran: false, warmup, fingerprint };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resetWarmupRuntimeCache() {
|
|
82
|
+
warmupFingerprintCache.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function shouldRunWarmup(warmup, fingerprint) {
|
|
86
|
+
if (!warmup.enabled) return false;
|
|
87
|
+
if (warmup.policy === 'never') return false;
|
|
88
|
+
if (warmup.policy === 'always') return true;
|
|
89
|
+
return !hasWarmupFingerprint(warmup.surface, fingerprint);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasWarmupFingerprint(surface, fingerprint) {
|
|
93
|
+
return warmupFingerprintCache.get(surface)?.has(fingerprint) ?? false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function markWarmupComplete(surface, fingerprint) {
|
|
97
|
+
let entries = warmupFingerprintCache.get(surface);
|
|
98
|
+
if (!entries) {
|
|
99
|
+
entries = new Map();
|
|
100
|
+
warmupFingerprintCache.set(surface, entries);
|
|
101
|
+
}
|
|
102
|
+
entries.set(fingerprint, Date.now());
|
|
103
|
+
while (entries.size > MAX_CACHE_ENTRIES_PER_SURFACE) {
|
|
104
|
+
const firstKey = entries.keys().next().value;
|
|
105
|
+
entries.delete(firstKey);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createWarmupFingerprint(options, { surface, steps }) {
|
|
110
|
+
return stableStringify({
|
|
111
|
+
surface,
|
|
112
|
+
steps,
|
|
113
|
+
seed: finiteOrNull(options.seed),
|
|
114
|
+
trackSeed: finiteOrNull(options.trackSeed),
|
|
115
|
+
totalLaps: finiteOrNull(options.totalLaps),
|
|
116
|
+
trackGeneration: options.trackGeneration ?? null,
|
|
117
|
+
track: summarizeTrack(options.track),
|
|
118
|
+
physicsMode: options.physicsMode ?? null,
|
|
119
|
+
trackQueryIndex: options.trackQueryIndex !== false,
|
|
120
|
+
rules: options.rules ?? null,
|
|
121
|
+
participantInteractions: options.participantInteractions ?? null,
|
|
122
|
+
replayGhosts: summarizeReplayGhosts(options.replayGhosts),
|
|
123
|
+
drivers: summarizeDrivers(options.drivers),
|
|
124
|
+
entries: summarizeEntries(options.entries),
|
|
125
|
+
controlledDrivers: summarizeStringArray(options.controlledDrivers),
|
|
126
|
+
sensors: options.sensors ?? null,
|
|
127
|
+
sensorsByDriver: summarizeSensorsByDriver(options.sensorsByDriver),
|
|
128
|
+
scenario: summarizeScenario(options.scenario),
|
|
129
|
+
frameSkip: finiteOrNull(options.frameSkip),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeWarmupPolicy(value, enabled) {
|
|
134
|
+
if (!enabled) return 'never';
|
|
135
|
+
if (typeof value !== 'string') return DEFAULT_WARMUP_POLICY;
|
|
136
|
+
return SUPPORTED_WARMUP_POLICIES.has(value) ? value : DEFAULT_WARMUP_POLICY;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeWarmupSteps(value, fallback) {
|
|
140
|
+
if (value == null) return fallback;
|
|
141
|
+
const steps = Math.floor(Number(value));
|
|
142
|
+
if (!Number.isFinite(steps) || steps < 1) return fallback;
|
|
143
|
+
return steps;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function summarizeTrack(track) {
|
|
147
|
+
if (!track || typeof track !== 'object') return null;
|
|
148
|
+
const samples = Array.isArray(track.samples) ? track.samples.length : null;
|
|
149
|
+
const centerlineControls = Array.isArray(track.centerlineControls) ? track.centerlineControls.length : null;
|
|
150
|
+
const drsZones = Array.isArray(track.drsZones) ? track.drsZones.length : null;
|
|
151
|
+
return {
|
|
152
|
+
name: track.name ?? null,
|
|
153
|
+
seed: finiteOrNull(track.seed),
|
|
154
|
+
length: finiteOrNull(track.length),
|
|
155
|
+
width: finiteOrNull(track.width),
|
|
156
|
+
sampleCount: finiteOrNull(track.sampleCount) ?? samples,
|
|
157
|
+
centerlineControls,
|
|
158
|
+
drsZones,
|
|
159
|
+
hasPitLane: Boolean(track.pitLane?.enabled),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function summarizeReplayGhosts(replayGhosts) {
|
|
164
|
+
if (!Array.isArray(replayGhosts)) return null;
|
|
165
|
+
return replayGhosts.map((ghost) => ({
|
|
166
|
+
id: ghost?.id ?? null,
|
|
167
|
+
visible: ghost?.visible !== false,
|
|
168
|
+
samples: Array.isArray(ghost?.trajectory) ? ghost.trajectory.length : 0,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function summarizeDrivers(drivers) {
|
|
173
|
+
if (!Array.isArray(drivers)) return null;
|
|
174
|
+
return {
|
|
175
|
+
count: drivers.length,
|
|
176
|
+
ids: drivers.map((driver) => driver?.id ?? null),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function summarizeEntries(entries) {
|
|
181
|
+
if (!Array.isArray(entries)) return null;
|
|
182
|
+
return {
|
|
183
|
+
count: entries.length,
|
|
184
|
+
driverIds: entries.map((entry) => entry?.driverId ?? null),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function summarizeStringArray(values) {
|
|
189
|
+
if (!Array.isArray(values)) return null;
|
|
190
|
+
return values.map((value) => String(value));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function summarizeSensorsByDriver(sensorsByDriver) {
|
|
194
|
+
if (!sensorsByDriver || typeof sensorsByDriver !== 'object') return null;
|
|
195
|
+
const ids = Object.keys(sensorsByDriver);
|
|
196
|
+
return {
|
|
197
|
+
count: ids.length,
|
|
198
|
+
ids: ids.sort(),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function summarizeScenario(scenario) {
|
|
203
|
+
if (!scenario || typeof scenario !== 'object') return null;
|
|
204
|
+
return {
|
|
205
|
+
participants: scenario.participants ?? null,
|
|
206
|
+
preset: scenario.preset ?? null,
|
|
207
|
+
placements: scenario.placements ?? null,
|
|
208
|
+
traffic: scenario.traffic ?? null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function stableStringify(value) {
|
|
213
|
+
if (value == null) return 'null';
|
|
214
|
+
if (typeof value === 'number' || typeof value === 'boolean') return JSON.stringify(value);
|
|
215
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
216
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
217
|
+
if (typeof value !== 'object') return JSON.stringify(String(value));
|
|
218
|
+
const keys = Object.keys(value).sort();
|
|
219
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function finiteOrNull(value) {
|
|
223
|
+
return Number.isFinite(Number(value)) ? Number(value) : null;
|
|
224
|
+
}
|
|
@@ -1,338 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { metersToSimUnits } from './units.js';
|
|
3
|
-
import { getVehicleGeometryState } from './vehicleGeometry.js';
|
|
4
|
-
|
|
5
|
-
const SURFACE_PRIORITY = {
|
|
6
|
-
track: 0,
|
|
7
|
-
'pit-entry': 1,
|
|
8
|
-
'pit-exit': 1,
|
|
9
|
-
'pit-lane': 2,
|
|
10
|
-
'pit-box': 3,
|
|
11
|
-
kerb: 4,
|
|
12
|
-
grass: 5,
|
|
13
|
-
gravel: 6,
|
|
14
|
-
barrier: 7,
|
|
15
|
-
};
|
|
16
|
-
const PIT_CONNECTOR_FULL_SAMPLE_WINDOW = metersToSimUnits(35);
|
|
17
|
-
|
|
18
|
-
function finiteSignatureValue(value) {
|
|
19
|
-
return Number.isFinite(value) ? Number(value).toFixed(4) : '';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function centerStateSignature(centerState) {
|
|
23
|
-
if (!centerState) return 'auto';
|
|
24
|
-
return [
|
|
25
|
-
finiteSignatureValue(centerState.distance),
|
|
26
|
-
finiteSignatureValue(centerState.signedOffset),
|
|
27
|
-
finiteSignatureValue(centerState.crossTrackError),
|
|
28
|
-
centerState.surface ?? '',
|
|
29
|
-
centerState.inPitLane ? 1 : 0,
|
|
30
|
-
centerState.pitLanePart ?? '',
|
|
31
|
-
centerState.pitBoxId ?? '',
|
|
32
|
-
].join(':');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function priority(surface) {
|
|
36
|
-
return SURFACE_PRIORITY[surface] ?? SURFACE_PRIORITY.barrier;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function worstState(states) {
|
|
40
|
-
return states.reduce((worst, state) => (
|
|
41
|
-
priority(state.surface) > priority(worst.surface) ? state : worst
|
|
42
|
-
), states[0]);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function patchSamples(patch) {
|
|
46
|
-
return [patch.center, ...patch.corners];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function wrappedDistanceDelta(first, second, length) {
|
|
50
|
-
if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(length) || length <= 0) return Infinity;
|
|
51
|
-
const delta = Math.abs(first - second);
|
|
52
|
-
return Math.min(delta, length - delta);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isNearPitConnector(track, centerState) {
|
|
56
|
-
const pitLane = track.pitLane;
|
|
57
|
-
if (!pitLane?.enabled) return false;
|
|
58
|
-
const distance = centerState.distance;
|
|
59
|
-
const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
|
|
60
|
-
const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
|
|
61
|
-
return wrappedDistanceDelta(distance, entryDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW ||
|
|
62
|
-
wrappedDistanceDelta(distance, exitDistance, track.length) <= PIT_CONNECTOR_FULL_SAMPLE_WINDOW;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function mainTrackSurface(track, crossTrackError) {
|
|
66
|
-
const trackEdge = track.width / 2;
|
|
67
|
-
const kerbEdge = trackEdge + (track.kerbWidth ?? 0);
|
|
68
|
-
const gravelEdge = kerbEdge + track.gravelWidth;
|
|
69
|
-
const runoffEdge = gravelEdge + track.runoffWidth;
|
|
70
|
-
if (crossTrackError <= trackEdge) return 'track';
|
|
71
|
-
if (crossTrackError <= kerbEdge) return 'kerb';
|
|
72
|
-
if (crossTrackError <= gravelEdge) return 'gravel';
|
|
73
|
-
if (crossTrackError <= runoffEdge) return 'grass';
|
|
74
|
-
return 'barrier';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function localTrackState(track, centerState, point) {
|
|
78
|
-
const signedOffset =
|
|
79
|
-
(point.x - centerState.x) * centerState.normalX +
|
|
80
|
-
(point.y - centerState.y) * centerState.normalY;
|
|
81
|
-
const crossTrackError = Math.abs(signedOffset);
|
|
82
|
-
const surface = mainTrackSurface(track, crossTrackError);
|
|
83
|
-
return {
|
|
84
|
-
...centerState,
|
|
85
|
-
signedOffset,
|
|
86
|
-
crossTrackError,
|
|
87
|
-
surface,
|
|
88
|
-
onTrack: surface === 'track' || surface === 'kerb',
|
|
89
|
-
inPitLane: false,
|
|
90
|
-
pitLanePart: null,
|
|
91
|
-
pitBoxId: null,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function wheelFullyOutside(samples, trackLimit) {
|
|
96
|
-
const rightOutside = samples.every((state) => !state.inPitLane && state.signedOffset > trackLimit);
|
|
97
|
-
const leftOutside = samples.every((state) => !state.inPitLane && state.signedOffset < -trackLimit);
|
|
98
|
-
return {
|
|
99
|
-
rightOutside,
|
|
100
|
-
leftOutside,
|
|
101
|
-
fullyOutsideWhiteLine: rightOutside || leftOutside,
|
|
102
|
-
outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit) {
|
|
107
|
-
const rightOutside = minimumSignedOffset > trackLimit;
|
|
108
|
-
const leftOutside = maximumSignedOffset < -trackLimit;
|
|
109
|
-
return {
|
|
110
|
-
rightOutside,
|
|
111
|
-
leftOutside,
|
|
112
|
-
fullyOutsideWhiteLine: rightOutside || leftOutside,
|
|
113
|
-
outsideSide: rightOutside ? 1 : leftOutside ? -1 : 0,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function stateFromSignedOffset(centerState, track, signedOffset) {
|
|
118
|
-
const crossTrackError = Math.abs(signedOffset);
|
|
119
|
-
const surface = mainTrackSurface(track, crossTrackError);
|
|
120
|
-
return {
|
|
121
|
-
...centerState,
|
|
122
|
-
signedOffset,
|
|
123
|
-
crossTrackError,
|
|
124
|
-
surface,
|
|
125
|
-
onTrack: surface === 'track' || surface === 'kerb',
|
|
126
|
-
inPitLane: false,
|
|
127
|
-
pitLanePart: null,
|
|
128
|
-
pitBoxId: null,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function analyticWheelState(patch, centerState, track, trackLimit) {
|
|
133
|
-
const wheelCenterOffset =
|
|
134
|
-
(patch.center.x - centerState.x) * centerState.normalX +
|
|
135
|
-
(patch.center.y - centerState.y) * centerState.normalY;
|
|
136
|
-
const projectedHalfWidth =
|
|
137
|
-
Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
|
|
138
|
-
Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
|
|
139
|
-
const minimumSignedOffset = wheelCenterOffset - projectedHalfWidth;
|
|
140
|
-
const maximumSignedOffset = wheelCenterOffset + projectedHalfWidth;
|
|
141
|
-
const signedOffset = Math.abs(minimumSignedOffset) > Math.abs(maximumSignedOffset)
|
|
142
|
-
? minimumSignedOffset
|
|
143
|
-
: maximumSignedOffset;
|
|
144
|
-
const state = stateFromSignedOffset(centerState, track, signedOffset);
|
|
145
|
-
const outside = wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit);
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
id: patch.id,
|
|
149
|
-
x: patch.center.x,
|
|
150
|
-
y: patch.center.y,
|
|
151
|
-
signedOffset: state.signedOffset,
|
|
152
|
-
crossTrackError: state.crossTrackError,
|
|
153
|
-
surface: state.surface,
|
|
154
|
-
onTrack: Boolean(state.onTrack),
|
|
155
|
-
inPitLane: false,
|
|
156
|
-
pitLanePart: null,
|
|
157
|
-
pitBoxId: null,
|
|
158
|
-
minimumSignedOffset,
|
|
159
|
-
maximumSignedOffset,
|
|
160
|
-
fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
|
|
161
|
-
outsideSide: outside.outsideSide,
|
|
162
|
-
sampledStates: [state],
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function patchPitOffsetRange(patch, centerState) {
|
|
167
|
-
const wheelCenterOffset =
|
|
168
|
-
(patch.center.x - centerState.x) * centerState.normalX +
|
|
169
|
-
(patch.center.y - centerState.y) * centerState.normalY;
|
|
170
|
-
const projectedHalfWidth =
|
|
171
|
-
Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
|
|
172
|
-
Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
|
|
173
|
-
return {
|
|
174
|
-
minimum: wheelCenterOffset - projectedHalfWidth,
|
|
175
|
-
maximum: wheelCenterOffset + projectedHalfWidth,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function canUseAnalyticPitWheels(geometry, centerState) {
|
|
180
|
-
const roadWidth = Number(centerState?.pitLaneRoadWidth);
|
|
181
|
-
if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
|
|
182
|
-
const halfRoadWidth = roadWidth / 2;
|
|
183
|
-
return geometry.contactPatches.every((patch) => {
|
|
184
|
-
const range = patchPitOffsetRange(patch, centerState);
|
|
185
|
-
return range.minimum >= -halfRoadWidth - 0.001 && range.maximum <= halfRoadWidth + 0.001;
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function analyticPitWheelState(patch, centerState) {
|
|
190
|
-
const range = patchPitOffsetRange(patch, centerState);
|
|
191
|
-
return {
|
|
192
|
-
id: patch.id,
|
|
193
|
-
x: patch.center.x,
|
|
194
|
-
y: patch.center.y,
|
|
195
|
-
signedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
|
|
196
|
-
crossTrackError: centerState.mainTrackCrossTrackError ?? centerState.crossTrackError,
|
|
197
|
-
surface: centerState.surface,
|
|
198
|
-
onTrack: true,
|
|
199
|
-
inPitLane: true,
|
|
200
|
-
pitLanePart: centerState.pitLanePart ?? null,
|
|
201
|
-
pitBoxId: centerState.pitBoxId ?? null,
|
|
202
|
-
minimumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
|
|
203
|
-
maximumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
|
|
204
|
-
pitLaneMinimumSignedOffset: range.minimum,
|
|
205
|
-
pitLaneMaximumSignedOffset: range.maximum,
|
|
206
|
-
fullyOutsideWhiteLine: false,
|
|
207
|
-
outsideSide: 0,
|
|
208
|
-
sampledStates: [centerState],
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function getEffectiveSurface(wheels = []) {
|
|
213
|
-
if (!wheels.length) return 'track';
|
|
214
|
-
return wheels.reduce((surface, wheel) => (
|
|
215
|
-
priority(wheel.surface) > priority(surface) ? wheel.surface : surface
|
|
216
|
-
), 'track');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function isWholeCarOutsideTrackLimits(wheels = [], track, relaxedMargin = 0) {
|
|
220
|
-
if (!wheels.length || !track) {
|
|
221
|
-
return {
|
|
222
|
-
violating: false,
|
|
223
|
-
side: 0,
|
|
224
|
-
outsideBy: 0,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
const trackLimit = track.width / 2 + relaxedMargin;
|
|
228
|
-
const right = wheels.every((wheel) => wheel.outsideSide === 1);
|
|
229
|
-
const left = wheels.every((wheel) => wheel.outsideSide === -1);
|
|
230
|
-
if (!right && !left) {
|
|
231
|
-
return {
|
|
232
|
-
violating: false,
|
|
233
|
-
side: 0,
|
|
234
|
-
outsideBy: 0,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
const side = right ? 1 : -1;
|
|
238
|
-
const outsideBy = right
|
|
239
|
-
? Math.min(...wheels.map((wheel) => wheel.minimumSignedOffset)) - trackLimit
|
|
240
|
-
: -trackLimit - Math.max(...wheels.map((wheel) => wheel.maximumSignedOffset));
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
violating: outsideBy > 0,
|
|
244
|
-
side,
|
|
245
|
-
outsideBy: Math.max(0, outsideBy),
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function calculateWheelSurfaceState({ car, track, centerState: providedCenterState = null }) {
|
|
250
|
-
const geometry = getVehicleGeometryState(car);
|
|
251
|
-
const trackLimit = track.width / 2;
|
|
252
|
-
const centerState = providedCenterState ?? nearestTrackState(track, car, car.progress);
|
|
253
|
-
const useAnalyticPitSampling = canUseAnalyticPitWheels(geometry, centerState);
|
|
254
|
-
const useFullSampling = !useAnalyticPitSampling && Boolean(centerState.inPitLane || isNearPitConnector(track, centerState));
|
|
255
|
-
const wheels = useAnalyticPitSampling
|
|
256
|
-
? geometry.contactPatches.map((patch) => analyticPitWheelState(patch, centerState))
|
|
257
|
-
: useFullSampling
|
|
258
|
-
? geometry.contactPatches.map((patch) => {
|
|
259
|
-
const sampleState = (point) => nearestTrackState(track, point, car.progress);
|
|
260
|
-
const sampledStates = patchSamples(patch).map(sampleState);
|
|
261
|
-
const state = worstState(sampledStates);
|
|
262
|
-
const offsets = sampledStates.map((sample) => sample.signedOffset);
|
|
263
|
-
const outside = wheelFullyOutside(sampledStates, trackLimit);
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
id: patch.id,
|
|
267
|
-
x: patch.center.x,
|
|
268
|
-
y: patch.center.y,
|
|
269
|
-
signedOffset: state.signedOffset,
|
|
270
|
-
crossTrackError: state.crossTrackError,
|
|
271
|
-
surface: state.surface,
|
|
272
|
-
onTrack: Boolean(state.onTrack),
|
|
273
|
-
inPitLane: Boolean(state.inPitLane),
|
|
274
|
-
pitLanePart: state.pitLanePart ?? null,
|
|
275
|
-
pitBoxId: state.pitBoxId ?? null,
|
|
276
|
-
minimumSignedOffset: Math.min(...offsets),
|
|
277
|
-
maximumSignedOffset: Math.max(...offsets),
|
|
278
|
-
fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
|
|
279
|
-
outsideSide: outside.outsideSide,
|
|
280
|
-
sampledStates,
|
|
281
|
-
};
|
|
282
|
-
})
|
|
283
|
-
: geometry.contactPatches.map((patch) => analyticWheelState(patch, centerState, track, trackLimit));
|
|
284
|
-
const effectiveSurface = getEffectiveSurface(wheels);
|
|
285
|
-
const representative = wheels.reduce((best, wheel) => (
|
|
286
|
-
Math.abs(wheel.signedOffset) > Math.abs(best.signedOffset) ? wheel : best
|
|
287
|
-
), wheels[0]);
|
|
288
|
-
const trackLimits = isWholeCarOutsideTrackLimits(wheels, track);
|
|
289
|
-
|
|
290
|
-
return {
|
|
291
|
-
wheels,
|
|
292
|
-
effectiveSurface,
|
|
293
|
-
trackLimits,
|
|
294
|
-
sampleMode: useAnalyticPitSampling ? 'pit-analytic' : useFullSampling ? 'full' : 'analytic',
|
|
295
|
-
representativeState: representative ? {
|
|
296
|
-
...centerState,
|
|
297
|
-
surface: effectiveSurface,
|
|
298
|
-
onTrack: wheels.every((wheel) => wheel.onTrack || wheel.inPitLane),
|
|
299
|
-
inPitLane: wheels.every((wheel) => wheel.inPitLane),
|
|
300
|
-
signedOffset: representative.signedOffset,
|
|
301
|
-
crossTrackError: representative.crossTrackError,
|
|
302
|
-
} : nearestTrackState(track, car, car.progress),
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function applyWheelSurfaceState(car, track, options = {}) {
|
|
307
|
-
const geometry = getVehicleGeometryState(car);
|
|
308
|
-
const cacheKey = {
|
|
309
|
-
track,
|
|
310
|
-
geometrySignature: geometry.signature,
|
|
311
|
-
centerSignature: centerStateSignature(options.centerState),
|
|
312
|
-
};
|
|
313
|
-
const cached = car.wheelSurfaceCache;
|
|
314
|
-
if (
|
|
315
|
-
cached?.track === cacheKey.track &&
|
|
316
|
-
cached.geometrySignature === cacheKey.geometrySignature &&
|
|
317
|
-
cached.centerSignature === cacheKey.centerSignature
|
|
318
|
-
) {
|
|
319
|
-
car.wheelStates = cached.result.wheels;
|
|
320
|
-
car.trackLimitState = cached.result.trackLimits;
|
|
321
|
-
car.trackState = cached.trackState;
|
|
322
|
-
return cached.result;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const result = calculateWheelSurfaceState({ car, track, centerState: options.centerState });
|
|
326
|
-
car.wheelStates = result.wheels;
|
|
327
|
-
car.trackLimitState = result.trackLimits;
|
|
328
|
-
car.trackState = {
|
|
329
|
-
...result.representativeState,
|
|
330
|
-
wheelSurface: result.effectiveSurface,
|
|
331
|
-
};
|
|
332
|
-
car.wheelSurfaceCache = {
|
|
333
|
-
...cacheKey,
|
|
334
|
-
result,
|
|
335
|
-
trackState: car.trackState,
|
|
336
|
-
};
|
|
337
|
-
return result;
|
|
338
|
-
}
|
|
1
|
+
export * from './vehicle/wheelSurface.js';
|