@inventure71/paddockjs 4.2.0 → 4.3.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 +40 -0
- package/INSTALL_AND_UPDATE.md +24 -4
- package/README.md +31 -2
- package/docs/advanced_ai_pace.md +131 -0
- package/docs/advanced_physics_rebuild.md +166 -0
- package/docs/architecture.md +109 -30
- package/docs/cleanup_coverage.md +284 -0
- package/docs/component_inventory.md +31 -15
- package/docs/composable-layouts.md +2 -0
- package/docs/concepts.md +12 -1
- package/docs/custom_model_controller.md +1 -1
- package/docs/data_contract.md +33 -10
- package/docs/data_model.md +4 -2
- package/docs/efficiency_cleanup_2026-10-13.md +80 -0
- package/docs/grid_walk.md +84 -0
- package/docs/index.md +1 -0
- package/docs/keyboard_driving_fix.md +118 -0
- package/docs/limit_driving_2026-10-14.md +88 -0
- package/docs/maintainability_audit_2026-09-08.md +146 -0
- package/docs/racing_ai_improvements.md +145 -0
- package/docs/rules.md +43 -13
- package/docs/sense_contract.md +4 -1
- package/docs/system_specs.md +38 -11
- package/docs/testing-reliability-guide.md +5 -0
- package/docs/v11_improvements.md +108 -0
- package/package.json +24 -13
- package/src/api/MountedSimulatorComponents.js +101 -0
- package/src/api/PaddockSimulatorController.js +56 -81
- package/src/app/BrowserExpertAdapter.js +0 -6
- package/src/app/BrowserExpertExternalRenderer.js +72 -50
- package/src/app/ExternalTrackSnapshot.js +50 -0
- package/src/app/F1SimulatorApp.js +242 -297
- package/src/app/HostLifecycleEmitter.js +93 -0
- package/src/app/banners/raceDataBannerController.js +9 -4
- package/src/app/camera/cameraConstants.js +1 -1
- package/src/app/camera/cameraController.js +10 -13
- package/src/app/camera/cameraGeometry.js +1 -1
- package/src/app/controls/SimulatorViewControls.js +45 -0
- package/src/app/controls/workbenchBindings.js +64 -0
- package/src/app/controls/workbenchDisclosure.js +41 -0
- package/src/app/domBindings.js +21 -13
- package/src/app/gridWalk/GridWalkController.js +180 -0
- package/src/app/gridWalk/gridWalkCamera.js +88 -0
- package/src/app/gridWalk/gridWalkControls.js +24 -0
- package/src/app/gridWalk/gridWalkCrew.js +246 -0
- package/src/app/gridWalk/gridWalkLabels.js +104 -0
- package/src/app/gridWalk/gridWalkLayout.js +46 -0
- package/src/app/gridWalk/gridWalkNavigation.js +106 -0
- package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
- package/src/app/gridWalk/gridWalkSession.js +50 -0
- package/src/app/layoutSupport.js +2 -0
- package/src/app/raceOverlayClearanceSupport.js +21 -7
- package/src/app/readouts/carOverviewRenderer.js +9 -4
- package/src/app/readouts/readoutFormatters.js +1 -8
- package/src/app/readouts/telemetryRenderer.js +1 -3
- package/src/app/readouts/timingTowerRenderer.js +48 -50
- package/src/app/rendering/appAssets.js +8 -10
- package/src/app/rendering/carRenderer.js +27 -79
- package/src/app/rendering/displayUtils.js +0 -6
- package/src/app/rendering/expertSensorRenderer.js +1 -1
- package/src/app/rendering/replayGhostRenderer.js +4 -1
- package/src/app/rendering/trackRenderer.js +2 -2
- package/src/app/runtime/frameLoop.js +18 -17
- package/src/app/runtime/pixiStartupLifecycle.js +94 -0
- package/src/app/runtime/runtimeTiming.js +4 -1
- package/src/app/runtime/runtimeVisibility.js +7 -2
- package/src/config/cssValues.js +1 -1
- package/src/config/defaultOptions.js +12 -173
- package/src/config/restartOptions.js +1 -0
- package/src/config/runtimeSeed.js +10 -0
- package/src/config/simulatorDefaults.js +106 -0
- package/src/config/telemetryModules.js +20 -0
- package/src/config/theme/cssTokens.js +85 -0
- package/src/config/theme/themeCss.js +72 -0
- package/src/config/theme/themeInputs.js +155 -0
- package/src/config/theme/themeResolver.js +172 -0
- package/src/config/theme/themeSelectors.js +69 -0
- package/src/config/theme/themeTokens.js +257 -0
- package/src/config/theme/themeUtils.js +15 -0
- package/src/config/themeOptions.js +14 -830
- package/src/config/timingGapMode.js +0 -2
- package/src/config/uiOptions.js +53 -0
- package/src/data/championship.js +16 -158
- package/src/data/championshipDefaults.js +87 -0
- package/src/data/driverData.js +10 -20
- package/src/data/entryIdentity.js +67 -0
- package/src/data/entryPresentation.js +23 -0
- package/src/data/entryTypes.d.ts +160 -0
- package/src/data/index.d.ts +13 -259
- package/src/data/ratingScale.js +14 -0
- package/src/data/teamTypes.d.ts +23 -0
- package/src/data/trackTypes.d.ts +101 -0
- package/src/data/vehicleData.js +12 -23
- package/src/environment/actions.js +3 -2
- package/src/environment/controllerLoop.js +57 -44
- package/src/environment/controllerScheduler.js +30 -0
- package/src/environment/driverObservations.js +260 -0
- package/src/environment/driverReset.js +51 -0
- package/src/environment/episode.js +0 -1
- package/src/environment/evaluation.js +6 -90
- package/src/environment/evaluationTracker.js +100 -0
- package/src/environment/events.js +23 -0
- package/src/environment/index.d.ts +21 -140
- package/src/environment/metrics.js +3 -17
- package/src/environment/observationSchema.js +104 -0
- package/src/environment/observationVector.js +16 -115
- package/src/environment/observations.js +16 -316
- package/src/environment/options.js +5 -24
- package/src/environment/renderFrames.js +21 -0
- package/src/environment/resetOptions.js +41 -0
- package/src/environment/results.js +125 -0
- package/src/environment/rewards.js +10 -16
- package/src/environment/runtime.js +17 -300
- package/src/environment/scenarioOptions.js +90 -0
- package/src/environment/scenarioPlacements.js +70 -0
- package/src/environment/scenarioPresets.js +78 -0
- package/src/environment/scenarios.js +5 -236
- package/src/environment/scratch.js +18 -0
- package/src/environment/sensorOptions.js +20 -0
- package/src/environment/sensors/index.js +2 -556
- package/src/environment/sensors/indexedRayBands.js +0 -18
- package/src/environment/sensors/nearbyCars.js +18 -15
- package/src/environment/sensors/opponentRadar.js +3 -2
- package/src/environment/sensors/pitConnectorProximity.js +20 -0
- package/src/environment/sensors/rayBandTrace.js +1 -37
- package/src/environment/sensors/rayBatch.js +81 -0
- package/src/environment/sensors/rayConfig.js +26 -0
- package/src/environment/sensors/rayGeometry.js +0 -8
- package/src/environment/sensors/rayGuards.js +0 -6
- package/src/environment/sensors/rayOutputs.js +75 -0
- package/src/environment/sensors/rayQuery.js +110 -0
- package/src/environment/sensors/rayScratch.js +146 -0
- package/src/environment/sensors/sensorTargets.js +12 -4
- package/src/environment/sensors/surfaceRays.js +6 -61
- package/src/environment/sensors/trackRays.js +7 -80
- package/src/environment/simulationSetup.js +30 -0
- package/src/environment/specs.js +15 -109
- package/src/index.d.ts +34 -259
- package/src/index.js +28 -6
- package/src/placeholder/index.js +1 -8
- package/src/rendering/displayLifecycle.js +6 -0
- package/src/rendering/proceduralTrackAsset.js +1 -1
- package/src/rendering/renderSnapshot.js +3 -0
- package/src/rendering/track/finishLineRenderer.js +1 -1
- package/src/rendering/track/gridRenderer.js +1 -1
- package/src/rendering/track/offsetStrokeSafety.js +2 -2
- package/src/rendering/track/pitLaneRenderer.js +1 -1
- package/src/rendering/track/trackMaterialRenderer.js +4 -4
- package/src/rendering/track/trackRenderConstants.js +0 -1
- package/src/rendering/track/trackRenderGeometry.js +1 -7
- package/src/simulation/collisionGeometry.js +5 -11
- package/src/simulation/driver/advancedPathControls.js +53 -0
- package/src/simulation/driver/advancedRacePace.js +112 -0
- package/src/simulation/driver/advancedRacingControls.js +61 -0
- package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
- package/src/simulation/driver/advancedRacingSteering.js +76 -0
- package/src/simulation/driver/advancedRacingTiming.js +16 -0
- package/src/simulation/driver/arcadeRacePace.js +83 -0
- package/src/simulation/driver/arcadeRacingControls.js +111 -0
- package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
- package/src/simulation/driver/driverControlConstants.js +0 -2
- package/src/simulation/driver/driverController.js +14 -3
- package/src/simulation/driver/edgeRecovery.js +0 -1
- package/src/simulation/driver/racingControls.js +10 -406
- package/src/simulation/driver/racingDriverProfile.js +48 -0
- package/src/simulation/driver/racingLinePlan.js +48 -15
- package/src/simulation/driver/rejoinControls.js +58 -111
- package/src/simulation/driver/safetyCarControls.js +12 -0
- package/src/simulation/driver/trafficScan.js +89 -6
- package/src/simulation/pit/pitCarPlacement.js +29 -0
- package/src/simulation/pit/pitFlow.js +8 -30
- package/src/simulation/pit/pitIntent.js +4 -3
- package/src/simulation/pit/pitOccupancy.js +2 -2
- package/src/simulation/pit/pitQueue.js +2 -18
- package/src/simulation/pit/pitRouteMovement.js +1 -36
- package/src/simulation/pit/pitRouting.js +5 -81
- package/src/simulation/pit/pitService.js +3 -32
- package/src/simulation/pit/pitServiceConstants.js +1 -0
- package/src/simulation/pit/pitServiceProgress.js +34 -0
- package/src/simulation/pit/pitSnapshots.js +8 -4
- package/src/simulation/pit/pitState.js +1 -5
- package/src/simulation/race/classification.js +1 -8
- package/src/simulation/race/raceFinish.js +3 -8
- package/src/simulation/race/raceOrder.js +2 -13
- package/src/simulation/race/raceProgress.js +6 -20
- package/src/simulation/race/raceSetup.js +1 -1
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/redFlag.js +2 -4
- package/src/simulation/race/safetyCar.js +3 -8
- package/src/simulation/race/stalledDnf.js +2 -4
- package/src/simulation/raceSimulation.js +6 -38
- package/src/simulation/replay/replayGhosts.js +44 -32
- package/src/simulation/rules/collisionSteward.js +0 -14
- package/src/simulation/rules/penaltyActions.js +37 -0
- package/src/simulation/rules/penaltyLedger.js +8 -1
- package/src/simulation/rules/rulesReview.js +1 -7
- package/src/simulation/simMath.js +4 -0
- package/src/simulation/snapshots/raceSnapshots.js +30 -11
- package/src/simulation/timing/drsState.js +8 -0
- package/src/simulation/timing/drsTiming.js +2 -4
- package/src/simulation/timing/lapTelemetry.js +7 -4
- package/src/simulation/timing/sectorPerformance.js +3 -3
- package/src/simulation/timing/timingLines.js +12 -6
- package/src/simulation/track/pitLaneAccess.js +2 -2
- package/src/simulation/track/pitLaneLayout.js +4 -135
- package/src/simulation/track/pitLaneModel.js +70 -0
- package/src/simulation/track/pitLaneState.js +0 -1
- package/src/simulation/track/pitServiceLayout.js +121 -0
- package/src/simulation/track/proceduralTrack.js +77 -0
- package/src/simulation/track/spatialQueries.js +1 -25
- package/src/simulation/track/trackBuilder.js +54 -0
- package/src/simulation/track/trackConstants.js +0 -22
- package/src/simulation/track/trackMath.js +4 -29
- package/src/simulation/track/trackModel.js +4 -241
- package/src/simulation/track/trackModelCache.js +105 -0
- package/src/simulation/track/trackNearestQueries.js +1218 -0
- package/src/simulation/track/trackQueryConstants.js +3 -0
- package/src/simulation/track/trackQueryConstruction.js +368 -0
- package/src/simulation/track/trackQueryIndex.js +12 -1933
- package/src/simulation/track/trackQuerySegments.js +174 -0
- package/src/simulation/track/trackQueryStats.js +12 -0
- package/src/simulation/track/trackRayQueries.js +165 -0
- package/src/simulation/vehicle/advancedContactResponse.js +89 -0
- package/src/simulation/vehicle/advancedTireForces.js +92 -0
- package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
- package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
- package/src/simulation/vehicle/contactResolution.js +20 -6
- package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
- package/src/simulation/vehicle/pitWheelSurface.js +0 -12
- package/src/simulation/vehicle/runoffResponse.js +2 -4
- package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
- package/src/simulation/vehicle/vehicleGeometry.js +0 -56
- package/src/simulation/vehicle/vehicleKinematics.js +10 -0
- package/src/simulation/vehicle/vehiclePhysics.js +35 -194
- package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
- package/src/simulation/vehicle/vehicleState.js +2 -1
- package/src/simulation/vehicle/wheelSurface.js +13 -432
- package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
- package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
- package/src/styles.css +173 -19
- package/src/ui/bannerTemplates.js +4 -2
- package/src/ui/cameraControlsTemplate.js +4 -2
- package/src/ui/gridWalkTemplate.js +29 -0
- package/src/ui/htmlEscaping.js +8 -0
- package/src/ui/shellTemplate.js +3 -0
- package/src/ui/telemetryDrawerTemplate.js +4 -2
- package/src/ui/telemetryTemplates.js +3 -22
- package/src/ui/templateUtils.js +2 -8
- package/src/ui/viewControlsTemplate.js +21 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { clamp } from '../simMath.js';
|
|
2
|
+
|
|
3
|
+
export function createCenterlineSegments(samples, segmentCount) {
|
|
4
|
+
const startX = new Float64Array(segmentCount);
|
|
5
|
+
const startY = new Float64Array(segmentCount);
|
|
6
|
+
const endX = new Float64Array(segmentCount);
|
|
7
|
+
const endY = new Float64Array(segmentCount);
|
|
8
|
+
const deltaX = new Float64Array(segmentCount);
|
|
9
|
+
const deltaY = new Float64Array(segmentCount);
|
|
10
|
+
const lengthSquared = new Float64Array(segmentCount);
|
|
11
|
+
const startDistance = new Float64Array(segmentCount);
|
|
12
|
+
const endDistance = new Float64Array(segmentCount);
|
|
13
|
+
const distanceSpan = new Float64Array(segmentCount);
|
|
14
|
+
const heading = new Float64Array(segmentCount);
|
|
15
|
+
const normalX = new Float64Array(segmentCount);
|
|
16
|
+
const normalY = new Float64Array(segmentCount);
|
|
17
|
+
const endNormalX = new Float64Array(segmentCount);
|
|
18
|
+
const endNormalY = new Float64Array(segmentCount);
|
|
19
|
+
const curvature = new Float64Array(segmentCount);
|
|
20
|
+
const minX = new Float64Array(segmentCount);
|
|
21
|
+
const maxX = new Float64Array(segmentCount);
|
|
22
|
+
const minY = new Float64Array(segmentCount);
|
|
23
|
+
const maxY = new Float64Array(segmentCount);
|
|
24
|
+
|
|
25
|
+
for (let id = 0; id < segmentCount; id += 1) {
|
|
26
|
+
const start = samples[id];
|
|
27
|
+
const end = samples[id + 1];
|
|
28
|
+
startX[id] = start.x;
|
|
29
|
+
startY[id] = start.y;
|
|
30
|
+
endX[id] = end.x;
|
|
31
|
+
endY[id] = end.y;
|
|
32
|
+
deltaX[id] = end.x - start.x;
|
|
33
|
+
deltaY[id] = end.y - start.y;
|
|
34
|
+
lengthSquared[id] = deltaX[id] * deltaX[id] + deltaY[id] * deltaY[id];
|
|
35
|
+
startDistance[id] = start.distance;
|
|
36
|
+
endDistance[id] = end.distance;
|
|
37
|
+
distanceSpan[id] = end.distance - start.distance;
|
|
38
|
+
heading[id] = start.heading;
|
|
39
|
+
normalX[id] = start.normalX;
|
|
40
|
+
normalY[id] = start.normalY;
|
|
41
|
+
endNormalX[id] = end.normalX;
|
|
42
|
+
endNormalY[id] = end.normalY;
|
|
43
|
+
curvature[id] = start.curvature ?? 0;
|
|
44
|
+
minX[id] = Math.min(start.x, end.x);
|
|
45
|
+
maxX[id] = Math.max(start.x, end.x);
|
|
46
|
+
minY[id] = Math.min(start.y, end.y);
|
|
47
|
+
maxY[id] = Math.max(start.y, end.y);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
segmentCount,
|
|
52
|
+
startX,
|
|
53
|
+
startY,
|
|
54
|
+
endX,
|
|
55
|
+
endY,
|
|
56
|
+
deltaX,
|
|
57
|
+
deltaY,
|
|
58
|
+
lengthSquared,
|
|
59
|
+
startDistance,
|
|
60
|
+
endDistance,
|
|
61
|
+
distanceSpan,
|
|
62
|
+
heading,
|
|
63
|
+
normalX,
|
|
64
|
+
normalY,
|
|
65
|
+
endNormalX,
|
|
66
|
+
endNormalY,
|
|
67
|
+
curvature,
|
|
68
|
+
minX,
|
|
69
|
+
maxX,
|
|
70
|
+
minY,
|
|
71
|
+
maxY,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createPrecomputedSegmentNeighborhoods(segmentCount, radii) {
|
|
76
|
+
if (!segmentCount || !radii?.size) return new Map();
|
|
77
|
+
const neighborhoodsByRadius = new Map();
|
|
78
|
+
for (const radius of radii) {
|
|
79
|
+
const width = radius * 2 + 1;
|
|
80
|
+
const neighborhoods = new Array(segmentCount);
|
|
81
|
+
for (let centerSegmentId = 0; centerSegmentId < segmentCount; centerSegmentId += 1) {
|
|
82
|
+
const segmentIds = new Int32Array(width);
|
|
83
|
+
for (let index = 0; index < width; index += 1) {
|
|
84
|
+
segmentIds[index] = ((centerSegmentId + (index - radius)) % segmentCount + segmentCount) % segmentCount;
|
|
85
|
+
}
|
|
86
|
+
neighborhoods[centerSegmentId] = segmentIds;
|
|
87
|
+
}
|
|
88
|
+
neighborhoodsByRadius.set(radius, neighborhoods);
|
|
89
|
+
}
|
|
90
|
+
return neighborhoodsByRadius;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getPrecomputedSegmentNeighborhood(centerline, centerSegmentId, radius) {
|
|
94
|
+
const neighborhoods = centerline?.precomputedNeighborhoodsByRadius?.get(radius);
|
|
95
|
+
return neighborhoods?.[centerSegmentId] ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function writeProjectionCandidate(target, {
|
|
99
|
+
segmentId,
|
|
100
|
+
x,
|
|
101
|
+
y,
|
|
102
|
+
distance,
|
|
103
|
+
heading,
|
|
104
|
+
normalX,
|
|
105
|
+
normalY,
|
|
106
|
+
curvature,
|
|
107
|
+
signedOffset,
|
|
108
|
+
distanceSquared,
|
|
109
|
+
}) {
|
|
110
|
+
target.segmentId = segmentId;
|
|
111
|
+
target.x = x;
|
|
112
|
+
target.y = y;
|
|
113
|
+
target.distance = distance;
|
|
114
|
+
target.heading = heading;
|
|
115
|
+
target.normalX = normalX;
|
|
116
|
+
target.normalY = normalY;
|
|
117
|
+
target.curvature = curvature;
|
|
118
|
+
target.signedOffset = signedOffset;
|
|
119
|
+
target.crossTrackError = Math.abs(signedOffset);
|
|
120
|
+
target.distanceSquared = distanceSquared;
|
|
121
|
+
return target;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function projectIndexedSegment(centerline, segmentId, position) {
|
|
125
|
+
if (centerline?.stats) centerline.stats.candidateProjectionObjectAllocations += 1;
|
|
126
|
+
const ax = centerline.startX[segmentId];
|
|
127
|
+
const ay = centerline.startY[segmentId];
|
|
128
|
+
const dx = centerline.deltaX[segmentId];
|
|
129
|
+
const dy = centerline.deltaY[segmentId];
|
|
130
|
+
const amount = centerline.lengthSquared[segmentId] > 0
|
|
131
|
+
? clamp(((position.x - ax) * dx + (position.y - ay) * dy) / centerline.lengthSquared[segmentId], 0, 1)
|
|
132
|
+
: 0;
|
|
133
|
+
const x = ax + dx * amount;
|
|
134
|
+
const y = ay + dy * amount;
|
|
135
|
+
const distance = centerline.startDistance[segmentId] +
|
|
136
|
+
centerline.distanceSpan[segmentId] * amount;
|
|
137
|
+
const heading = centerline.heading[segmentId];
|
|
138
|
+
const normalX = centerline.normalX[segmentId];
|
|
139
|
+
const normalY = centerline.normalY[segmentId];
|
|
140
|
+
const px = position.x - x;
|
|
141
|
+
const py = position.y - y;
|
|
142
|
+
const signedOffset = px * normalX + py * normalY;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
segmentId,
|
|
146
|
+
x,
|
|
147
|
+
y,
|
|
148
|
+
distance,
|
|
149
|
+
heading,
|
|
150
|
+
normalX,
|
|
151
|
+
normalY,
|
|
152
|
+
curvature: centerline.curvature[segmentId],
|
|
153
|
+
signedOffset,
|
|
154
|
+
crossTrackError: Math.abs(signedOffset),
|
|
155
|
+
distanceSquared: px * px + py * py,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function segmentFromIndex(centerline, segmentId) {
|
|
160
|
+
if (centerline?.stats) centerline.stats.raySegmentObjectAllocations += 1;
|
|
161
|
+
return {
|
|
162
|
+
segmentId,
|
|
163
|
+
startX: centerline.startX[segmentId],
|
|
164
|
+
startY: centerline.startY[segmentId],
|
|
165
|
+
endX: centerline.endX[segmentId],
|
|
166
|
+
endY: centerline.endY[segmentId],
|
|
167
|
+
startDistance: centerline.startDistance[segmentId],
|
|
168
|
+
endDistance: centerline.endDistance[segmentId],
|
|
169
|
+
normalX: centerline.normalX[segmentId],
|
|
170
|
+
normalY: centerline.normalY[segmentId],
|
|
171
|
+
endNormalX: centerline.endNormalX[segmentId],
|
|
172
|
+
endNormalY: centerline.endNormalY[segmentId],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -51,3 +51,15 @@ export function recordStat(index, bucket, key) {
|
|
|
51
51
|
target[key] = (target[key] ?? 0) + 1;
|
|
52
52
|
index.stats[bucket] = target;
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
export function resetTrackQueryStats(track) {
|
|
56
|
+
const stats = track?.queryIndex?.stats;
|
|
57
|
+
if (!stats) return null;
|
|
58
|
+
Object.assign(stats, createQueryStats());
|
|
59
|
+
return stats;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function snapshotTrackQueryStats(track) {
|
|
63
|
+
const stats = track?.queryIndex?.stats;
|
|
64
|
+
return stats ? JSON.parse(JSON.stringify(stats)) : null;
|
|
65
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { gridCellForPoint } from './trackQueryGrid.js';
|
|
2
|
+
import { ensureQueryScratch, ensureScratchArray, nextScratchEpoch } from './trackQueryScratch.js';
|
|
3
|
+
import { segmentFromIndex } from './trackQuerySegments.js';
|
|
4
|
+
import { finitePoint } from './trackMath.js';
|
|
5
|
+
|
|
6
|
+
const RAY_TRACE_BUCKET_DEGREES = 10;
|
|
7
|
+
|
|
8
|
+
export function queryTrackSegmentIdsAlongRay(track, origin, vector, maxDistance, margin = 0) {
|
|
9
|
+
const index = track?.queryIndex;
|
|
10
|
+
if (
|
|
11
|
+
!index?.centerline?.segmentCount ||
|
|
12
|
+
!finitePoint(origin) ||
|
|
13
|
+
!Number.isFinite(vector?.x) ||
|
|
14
|
+
!Number.isFinite(vector?.y) ||
|
|
15
|
+
!Number.isFinite(maxDistance) ||
|
|
16
|
+
maxDistance < 0
|
|
17
|
+
) return null;
|
|
18
|
+
|
|
19
|
+
const grid = index.segmentGrid ?? index.grid;
|
|
20
|
+
const scratch = ensureQueryScratch(index);
|
|
21
|
+
const segmentMarks = ensureScratchArray(scratch, 'raySegmentMarks', index.centerline.segmentCount);
|
|
22
|
+
const cellMarks = ensureScratchArray(scratch, 'rayCellMarks', grid.columns * grid.rows);
|
|
23
|
+
const segmentEpoch = nextScratchEpoch(scratch, 'raySegmentEpoch', segmentMarks);
|
|
24
|
+
const cellEpoch = nextScratchEpoch(scratch, 'rayCellEpoch', cellMarks);
|
|
25
|
+
const ids = scratch.raySegmentIds;
|
|
26
|
+
ids.length = 0;
|
|
27
|
+
const radius = Math.max(0, Math.ceil(Math.max(0, margin) / grid.cellSize));
|
|
28
|
+
const step = Math.max(grid.cellSize * 0.5, 1);
|
|
29
|
+
const sampleCount = Math.max(1, Math.ceil(maxDistance / step));
|
|
30
|
+
const visitedCells = scratch.rayVisitedCells;
|
|
31
|
+
visitedCells.length = 0;
|
|
32
|
+
const point = scratch.rayTracePoint ?? { x: 0, y: 0 };
|
|
33
|
+
scratch.rayTracePoint = point;
|
|
34
|
+
|
|
35
|
+
const seed = seedRayTraceFromNearbyCache(index, grid, origin, vector, radius, maxDistance, segmentMarks, segmentEpoch, cellMarks, cellEpoch, ids);
|
|
36
|
+
|
|
37
|
+
for (let sample = 0; sample <= sampleCount; sample += 1) {
|
|
38
|
+
const distance = Math.min(maxDistance, sample * step);
|
|
39
|
+
point.x = origin.x + vector.x * distance;
|
|
40
|
+
point.y = origin.y + vector.y * distance;
|
|
41
|
+
const center = gridCellForPoint(grid, point, false);
|
|
42
|
+
if (!center) continue;
|
|
43
|
+
for (let row = center.row - radius; row <= center.row + radius; row += 1) {
|
|
44
|
+
for (let column = center.column - radius; column <= center.column + radius; column += 1) {
|
|
45
|
+
if (column < 0 || row < 0 || column >= grid.columns || row >= grid.rows) continue;
|
|
46
|
+
const cellIndex = row * grid.columns + column;
|
|
47
|
+
if (cellMarks[cellIndex] === cellEpoch) continue;
|
|
48
|
+
cellMarks[cellIndex] = cellEpoch;
|
|
49
|
+
visitedCells.push(cellIndex);
|
|
50
|
+
const cell = grid.cells[cellIndex];
|
|
51
|
+
if (!cell) continue;
|
|
52
|
+
for (const segmentId of cell) {
|
|
53
|
+
if (segmentMarks[segmentId] === segmentEpoch) continue;
|
|
54
|
+
segmentMarks[segmentId] = segmentEpoch;
|
|
55
|
+
ids.push(segmentId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
storeRayTraceCache(index, grid, origin, vector, radius, maxDistance, ids, visitedCells, seed);
|
|
62
|
+
return ids;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function queryTrackSegmentsAlongRay(track, origin, vector, maxDistance, margin = 0) {
|
|
66
|
+
const index = track?.queryIndex;
|
|
67
|
+
const ids = queryTrackSegmentIdsAlongRay(track, origin, vector, maxDistance, margin);
|
|
68
|
+
if (!ids) return null;
|
|
69
|
+
if (!ids.length) return [];
|
|
70
|
+
return ids.map((segmentId) => segmentFromIndex(index.centerline, segmentId));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function seedRayTraceFromNearbyCache(
|
|
74
|
+
index,
|
|
75
|
+
grid,
|
|
76
|
+
origin,
|
|
77
|
+
vector,
|
|
78
|
+
radius,
|
|
79
|
+
maxDistance,
|
|
80
|
+
segmentMarks,
|
|
81
|
+
segmentEpoch,
|
|
82
|
+
cellMarks,
|
|
83
|
+
cellEpoch,
|
|
84
|
+
ids,
|
|
85
|
+
) {
|
|
86
|
+
const scratch = ensureQueryScratch(index);
|
|
87
|
+
const traceCache = scratch.rayTraceCache;
|
|
88
|
+
const originCell = gridCellForPoint(grid, origin, false);
|
|
89
|
+
if (!originCell) return null;
|
|
90
|
+
const bucket = rayTraceBucket(vector);
|
|
91
|
+
const base = rayTraceBaseKey(grid, originCell, radius);
|
|
92
|
+
const minimumSeedDistance = maxDistance * 0.85;
|
|
93
|
+
let seed = traceCache.get(rayTraceKey(base, bucket));
|
|
94
|
+
let bestSeedDistance = seed?.maxDistance ?? -Infinity;
|
|
95
|
+
if (bestSeedDistance < minimumSeedDistance) {
|
|
96
|
+
seed = null;
|
|
97
|
+
bestSeedDistance = -Infinity;
|
|
98
|
+
}
|
|
99
|
+
const beforeSeed = traceCache.get(rayTraceKey(base, bucket - 1));
|
|
100
|
+
if ((beforeSeed?.maxDistance ?? -Infinity) >= minimumSeedDistance && beforeSeed.maxDistance > bestSeedDistance) {
|
|
101
|
+
seed = beforeSeed;
|
|
102
|
+
bestSeedDistance = beforeSeed.maxDistance;
|
|
103
|
+
}
|
|
104
|
+
const afterSeed = traceCache.get(rayTraceKey(base, bucket + 1));
|
|
105
|
+
if ((afterSeed?.maxDistance ?? -Infinity) >= minimumSeedDistance && afterSeed.maxDistance > bestSeedDistance) {
|
|
106
|
+
seed = afterSeed;
|
|
107
|
+
}
|
|
108
|
+
if (!seed) return { base, bucket };
|
|
109
|
+
|
|
110
|
+
for (const segmentId of seed.segmentIds) {
|
|
111
|
+
if (segmentMarks[segmentId] === segmentEpoch) continue;
|
|
112
|
+
segmentMarks[segmentId] = segmentEpoch;
|
|
113
|
+
ids.push(segmentId);
|
|
114
|
+
}
|
|
115
|
+
for (const cellId of seed.cellIds) {
|
|
116
|
+
cellMarks[cellId] = cellEpoch;
|
|
117
|
+
}
|
|
118
|
+
return { base, bucket };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function storeRayTraceCache(index, grid, origin, vector, radius, maxDistance, segmentIds, cellIds, seed) {
|
|
122
|
+
const scratch = ensureQueryScratch(index);
|
|
123
|
+
const traceCache = scratch.rayTraceCache;
|
|
124
|
+
const originCell = gridCellForPoint(grid, origin, false);
|
|
125
|
+
if (!originCell) return;
|
|
126
|
+
const bucket = seed?.bucket ?? rayTraceBucket(vector);
|
|
127
|
+
const base = seed?.base ?? rayTraceBaseKey(grid, originCell, radius);
|
|
128
|
+
const key = rayTraceKey(base, bucket);
|
|
129
|
+
let entry = traceCache.get(key);
|
|
130
|
+
if (!entry) {
|
|
131
|
+
entry = {
|
|
132
|
+
maxDistance: 0,
|
|
133
|
+
segmentIds: [],
|
|
134
|
+
cellIds: [],
|
|
135
|
+
};
|
|
136
|
+
traceCache.set(key, entry);
|
|
137
|
+
if (index.stats) index.stats.rayTraceCacheEntryAllocations += 1;
|
|
138
|
+
}
|
|
139
|
+
entry.maxDistance = maxDistance;
|
|
140
|
+
entry.segmentIds.length = segmentIds.length;
|
|
141
|
+
for (let index = 0; index < segmentIds.length; index += 1) {
|
|
142
|
+
entry.segmentIds[index] = segmentIds[index];
|
|
143
|
+
}
|
|
144
|
+
entry.cellIds.length = cellIds.length;
|
|
145
|
+
for (let index = 0; index < cellIds.length; index += 1) {
|
|
146
|
+
entry.cellIds[index] = cellIds[index];
|
|
147
|
+
}
|
|
148
|
+
if (traceCache.size > 96) {
|
|
149
|
+
const oldestKey = traceCache.keys().next().value;
|
|
150
|
+
if (oldestKey) traceCache.delete(oldestKey);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function rayTraceBaseKey(grid, center, radius) {
|
|
155
|
+
return `${center.column}:${center.row}:${radius}:${grid.columns}:${grid.rows}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function rayTraceKey(base, bucket) {
|
|
159
|
+
return `${base}:${bucket}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function rayTraceBucket(vector) {
|
|
163
|
+
const angleDegrees = Math.atan2(vector.y, vector.x) * 180 / Math.PI;
|
|
164
|
+
return Math.round(angleDegrees / RAY_TRACE_BUCKET_DEGREES);
|
|
165
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { clamp } from '../simMath.js';
|
|
2
|
+
import { SIM_UNITS_PER_METER } from '../units.js';
|
|
3
|
+
import { ADVANCED_VEHICLE_MODEL } from './advancedVehicleModel.js';
|
|
4
|
+
import { VEHICLE_GEOMETRY } from './vehicleGeometry.js';
|
|
5
|
+
|
|
6
|
+
function inverseMass(car, fixed) {
|
|
7
|
+
if (fixed) return 0;
|
|
8
|
+
return 1 / (Number.isFinite(car.mass) && car.mass > 0 ? car.mass : 798);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function supportOffset(car, normalX, normalY, coordinate) {
|
|
12
|
+
const cos = Math.cos(car.heading);
|
|
13
|
+
const sin = Math.sin(car.heading);
|
|
14
|
+
const along = normalX * cos + normalY * sin;
|
|
15
|
+
const across = -normalX * sin + normalY * cos;
|
|
16
|
+
// A normal parallel to a face selects its center, rather than an arbitrary corner.
|
|
17
|
+
const longitudinal = Math.abs(along) < 1e-10 ? 0 : Math.sign(along) * VEHICLE_GEOMETRY.bodyLength / 2;
|
|
18
|
+
const lateral = Math.abs(across) < 1e-10 ? 0 : Math.sign(across) * VEHICLE_GEOMETRY.bodyWidth / 2;
|
|
19
|
+
return coordinate === 'x' ? cos * longitudinal - sin * lateral : sin * longitudinal + cos * lateral;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function velocityComponent(car, coordinate) {
|
|
23
|
+
if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
|
|
24
|
+
return coordinate === 'x' ? car.velocityX : car.velocityY;
|
|
25
|
+
}
|
|
26
|
+
return (coordinate === 'x' ? Math.cos(car.heading) : Math.sin(car.heading)) * car.speed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeVelocity(car, velocityX, velocityY, yawRate) {
|
|
30
|
+
car.velocityX = velocityX * SIM_UNITS_PER_METER;
|
|
31
|
+
car.velocityY = velocityY * SIM_UNITS_PER_METER;
|
|
32
|
+
car.speed = Math.hypot(car.velocityX, car.velocityY);
|
|
33
|
+
car.yawRate = yawRate;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function applyAdvancedContactResponse(first, second, axis, {
|
|
37
|
+
restitution = 0,
|
|
38
|
+
firstFixed = false,
|
|
39
|
+
secondFixed = false,
|
|
40
|
+
} = {}) {
|
|
41
|
+
const axisLength = Math.hypot(axis.x, axis.y);
|
|
42
|
+
if (!(axisLength > 0)) return 0;
|
|
43
|
+
const nx = axis.x / axisLength;
|
|
44
|
+
const ny = axis.y / axisLength;
|
|
45
|
+
const firstInverseMass = inverseMass(first, firstFixed);
|
|
46
|
+
const secondInverseMass = inverseMass(second, secondFixed);
|
|
47
|
+
if (firstInverseMass + secondInverseMass === 0) return 0;
|
|
48
|
+
const firstInverseInertia = firstInverseMass / ADVANCED_VEHICLE_MODEL.yawInertiaPerMass;
|
|
49
|
+
const secondInverseInertia = secondInverseMass / ADVANCED_VEHICLE_MODEL.yawInertiaPerMass;
|
|
50
|
+
|
|
51
|
+
let firstLever = 0;
|
|
52
|
+
let secondLever = 0;
|
|
53
|
+
if ([first.x, first.y, second.x, second.y].every(Number.isFinite)) {
|
|
54
|
+
// SAT supplies a normal but no clipped contact manifold. Approximate a common
|
|
55
|
+
// contact point by the midpoint of opposing body support features. This keeps
|
|
56
|
+
// equal/opposite impulses and their moments consistent without rewinding poses.
|
|
57
|
+
const dx = second.x - first.x;
|
|
58
|
+
const dy = second.y - first.y;
|
|
59
|
+
const supportX = supportOffset(first, nx, ny, 'x') + supportOffset(second, -nx, -ny, 'x');
|
|
60
|
+
const supportY = supportOffset(first, nx, ny, 'y') + supportOffset(second, -nx, -ny, 'y');
|
|
61
|
+
firstLever = ((dx + supportX) * ny - (dy + supportY) * nx) / (2 * SIM_UNITS_PER_METER);
|
|
62
|
+
secondLever = ((-dx + supportX) * ny - (-dy + supportY) * nx) / (2 * SIM_UNITS_PER_METER);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let firstVX = velocityComponent(first, 'x') / SIM_UNITS_PER_METER;
|
|
66
|
+
let firstVY = velocityComponent(first, 'y') / SIM_UNITS_PER_METER;
|
|
67
|
+
let secondVX = velocityComponent(second, 'x') / SIM_UNITS_PER_METER;
|
|
68
|
+
let secondVY = velocityComponent(second, 'y') / SIM_UNITS_PER_METER;
|
|
69
|
+
let firstYaw = first.yawRate ?? 0;
|
|
70
|
+
let secondYaw = second.yawRate ?? 0;
|
|
71
|
+
const closingVelocity = (secondVX - firstVX) * nx + (secondVY - firstVY) * ny +
|
|
72
|
+
secondYaw * secondLever - firstYaw * firstLever;
|
|
73
|
+
let impulse = 0;
|
|
74
|
+
if (closingVelocity < 0) {
|
|
75
|
+
const inverseEffectiveMass = firstInverseMass + secondInverseMass +
|
|
76
|
+
firstLever * firstLever * firstInverseInertia + secondLever * secondLever * secondInverseInertia;
|
|
77
|
+
impulse = -(1 + clamp(restitution, 0, 1)) * closingVelocity / inverseEffectiveMass;
|
|
78
|
+
firstVX -= impulse * nx * firstInverseMass;
|
|
79
|
+
firstVY -= impulse * ny * firstInverseMass;
|
|
80
|
+
secondVX += impulse * nx * secondInverseMass;
|
|
81
|
+
secondVY += impulse * ny * secondInverseMass;
|
|
82
|
+
firstYaw -= impulse * firstLever * firstInverseInertia;
|
|
83
|
+
secondYaw += impulse * secondLever * secondInverseInertia;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!firstFixed) writeVelocity(first, firstVX, firstVY, firstYaw);
|
|
87
|
+
if (!secondFixed) writeVelocity(second, secondVX, secondVY, secondYaw);
|
|
88
|
+
return impulse;
|
|
89
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { clamp } from '../simMath.js';
|
|
2
|
+
import { ADVANCED_VEHICLE_MODEL as MODEL, ADVANCED_WHEELS, advancedWheelSurface } from './advancedVehicleModel.js';
|
|
3
|
+
|
|
4
|
+
// Quasi-static load transfer: both axles conserve their total vertical load.
|
|
5
|
+
// A wheel can unload, but cannot pull down on the ground. This planar model
|
|
6
|
+
// does not simulate roll/pitch displacement or suspension travel.
|
|
7
|
+
export function advancedNormalLoads(car, speedMps, longitudinalAcceleration, lateralAcceleration) {
|
|
8
|
+
const weight = car.mass * MODEL.gravity;
|
|
9
|
+
const downforce = car.downforceCoefficient * MODEL.downforceScale * speedMps ** 2;
|
|
10
|
+
const aero = car.drsActive ? downforce * 0.82 : downforce;
|
|
11
|
+
const transfer = car.mass * longitudinalAcceleration * MODEL.centerOfGravityHeight / MODEL.wheelbase;
|
|
12
|
+
const total = weight + aero;
|
|
13
|
+
const front = clamp(weight * MODEL.frontWeightFraction + aero * MODEL.frontAeroFraction - transfer, 0, total);
|
|
14
|
+
const rear = total - front;
|
|
15
|
+
const lateralTransfer = car.mass * lateralAcceleration * MODEL.centerOfGravityHeight / MODEL.trackWidth;
|
|
16
|
+
const frontTransfer = clamp(lateralTransfer * MODEL.frontRollTransferFraction, -front / 2, front / 2);
|
|
17
|
+
const rearTransfer = clamp(lateralTransfer * (1 - MODEL.frontRollTransferFraction), -rear / 2, rear / 2);
|
|
18
|
+
return [front / 2 + frontTransfer, front / 2 - frontTransfer, rear / 2 + rearTransfer, rear / 2 - rearTransfer];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function advancedTireForce({ normalLoad, nominalLoad, grip, tireFactor, surface, longitudinalDemand, forwardVelocity, lateralVelocity }) {
|
|
22
|
+
if (normalLoad <= 0) return { longitudinal: 0, lateral: 0, usage: 0, capacity: 0, slipAngle: 0 };
|
|
23
|
+
const capacity = contactCapacity(normalLoad, nominalLoad, grip, tireFactor, surface);
|
|
24
|
+
const slipAngle = Math.atan2(lateralVelocity, Math.max(Math.abs(forwardVelocity), MODEL.lowSpeedSlipReference));
|
|
25
|
+
const stiffness = MODEL.corneringStiffnessPerLoad * nominalLoad * (normalLoad / nominalLoad) ** 0.8;
|
|
26
|
+
const lateralDemand = -stiffness * Math.tan(slipAngle);
|
|
27
|
+
const demand = Math.hypot(longitudinalDemand, lateralDemand);
|
|
28
|
+
const usage = demand / Math.max(capacity, 1e-9);
|
|
29
|
+
const longitudinal = clamp(longitudinalDemand, -capacity, capacity);
|
|
30
|
+
const lateralCapacity = Math.sqrt(Math.max(0, capacity ** 2 - longitudinal ** 2));
|
|
31
|
+
// Longitudinal input is a requested force, not a slip ratio. Reserve its
|
|
32
|
+
// friction-circle share; the brush curve applies to the lateral slip demand.
|
|
33
|
+
const q = lateralCapacity > 1e-9 ? Math.min(Math.abs(lateralDemand) / (3 * lateralCapacity), 1) : 1;
|
|
34
|
+
let lateral = Math.sign(lateralDemand) * lateralCapacity * (1 - (1 - q) ** 3);
|
|
35
|
+
let longitudinalForce = longitudinal;
|
|
36
|
+
// With brake demand beyond adhesion, a sliding contact opposes its actual
|
|
37
|
+
// velocity. This captures directional loss under lock without wheel-spin state.
|
|
38
|
+
if (Math.abs(longitudinalDemand) >= capacity && longitudinalDemand * forwardVelocity < 0) {
|
|
39
|
+
const speed = Math.hypot(forwardVelocity, lateralVelocity);
|
|
40
|
+
longitudinalForce = -capacity * forwardVelocity / speed;
|
|
41
|
+
lateral = -capacity * lateralVelocity / speed;
|
|
42
|
+
}
|
|
43
|
+
return { longitudinal: longitudinalForce, lateral, usage, capacity, slipAngle };
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Policy-facing capacity estimate; the driver chooses how much margin to keep.
|
|
48
|
+
// No controller gets additional grip beyond these same contact-patch capacities.
|
|
49
|
+
function contactCapacity(normalLoad, nominalLoad, grip, tireFactor, surface) {
|
|
50
|
+
return grip * MODEL.tireGripScale * tireFactor * surface.grip * nominalLoad *
|
|
51
|
+
(normalLoad / nominalLoad) ** MODEL.tireLoadExponent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { contactCapacity as advancedContactCapacity };
|
|
55
|
+
|
|
56
|
+
// Signed wheel-angle correction needed to produce the requested lateral
|
|
57
|
+
// force. Tire-local velocity slip has the opposite sign in advancedTireForce.
|
|
58
|
+
export function advancedLateralSlipAngle({ normalLoad, nominalLoad, longitudinalForce = 0, lateralForce, capacity }) {
|
|
59
|
+
if (!(normalLoad > 0) || !(nominalLoad > 0) || !(capacity > 0)) return 0;
|
|
60
|
+
const remaining = Math.sqrt(Math.max(0, capacity ** 2 - longitudinalForce ** 2));
|
|
61
|
+
if (!(remaining > 0)) return 0;
|
|
62
|
+
const q = 1 - Math.cbrt(1 - clamp(Math.abs(lateralForce) / remaining, 0, 1));
|
|
63
|
+
const lateralDemand = 3 * remaining * q;
|
|
64
|
+
const stiffness = MODEL.corneringStiffnessPerLoad * nominalLoad * (normalLoad / nominalLoad) ** 0.8;
|
|
65
|
+
return Math.sign(lateralForce) * Math.atan(lateralDemand / stiffness);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function summedCapacity(car, speedMps, tireFactor, rearOnly) {
|
|
69
|
+
const loads = advancedNormalLoads(car, speedMps, 0, 0);
|
|
70
|
+
let force = 0;
|
|
71
|
+
for (let index = 0; index < ADVANCED_WHEELS.length; index += 1) {
|
|
72
|
+
const wheel = ADVANCED_WHEELS[index];
|
|
73
|
+
if (rearOnly && wheel.front) continue;
|
|
74
|
+
const nominalLoad = car.mass * MODEL.gravity * (wheel.front ? MODEL.frontWeightFraction : 1 - MODEL.frontWeightFraction) / 2;
|
|
75
|
+
const surface = advancedWheelSurface(car, wheel.id);
|
|
76
|
+
force += contactCapacity(loads[index], nominalLoad, car.tireGrip, tireFactor, surface);
|
|
77
|
+
}
|
|
78
|
+
return force;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function advancedLateralAccelerationLimit(car, speedMps, tireFactor) {
|
|
82
|
+
return summedCapacity(car, speedMps, tireFactor, false) / car.mass;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function advancedRearForceCapacity(car, speedMps, tireFactor) {
|
|
86
|
+
return summedCapacity(car, speedMps, tireFactor, true);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function advancedRollingResistanceForce(car, speedMps) {
|
|
90
|
+
const loads = advancedNormalLoads(car, speedMps, 0, 0);
|
|
91
|
+
return ADVANCED_WHEELS.reduce((force, wheel, index) => force + advancedWheelSurface(car, wheel.id).rolling * loads[index], 0);
|
|
92
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { VEHICLE_GEOMETRY } from './vehicleGeometry.js';
|
|
2
|
+
import { simUnitsToMeters } from '../units.js';
|
|
3
|
+
|
|
4
|
+
// SI calibration for the package's DRS-era Formula-style car. Existing setup
|
|
5
|
+
// ratings remain force/grip parameters; this is not a particular team's dataset.
|
|
6
|
+
export const ADVANCED_VEHICLE_MODEL = Object.freeze({
|
|
7
|
+
gravity: 9.80665,
|
|
8
|
+
wheelbase: simUnitsToMeters(VEHICLE_GEOMETRY.wheelLongitudinalOffset * 2),
|
|
9
|
+
trackWidth: simUnitsToMeters(VEHICLE_GEOMETRY.wheelLateralOffset * 2),
|
|
10
|
+
frontWeightFraction: 0.5, // stored pose/velocity and collision origin are the chassis center
|
|
11
|
+
frontAeroFraction: 0.44,
|
|
12
|
+
frontRollTransferFraction: 0.52,
|
|
13
|
+
centerOfGravityHeight: 0.3,
|
|
14
|
+
yawInertiaPerMass: 1.65, // m²: yaw inertia = mass × radius of gyration squared
|
|
15
|
+
frontBrakeFraction: 0.57,
|
|
16
|
+
tireGripScale: 0.72,
|
|
17
|
+
tireLoadExponent: 0.9,
|
|
18
|
+
corneringStiffnessPerLoad: 28, // N/rad per N of normal load
|
|
19
|
+
maxIntegrationStep: 1 / 240,
|
|
20
|
+
lowSpeedSlipReference: 2.5, // m/s, regularizes the static tire limit
|
|
21
|
+
engineForceScale: 0.5, // legacy force rating to peak rear-axle wheel force
|
|
22
|
+
enginePowerSpeed: 28, // m/s: peak force × crossover speed gives the power envelope
|
|
23
|
+
airDensity: 1.225,
|
|
24
|
+
dragAreaScale: 4.5, // m² per existing dragCoefficient unit
|
|
25
|
+
downforceScale: 0.5, // existing coefficient × v² × scale gives newtons
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const ADVANCED_WHEELS = Object.freeze([
|
|
29
|
+
{ id: 'front-left', front: true, side: -1 },
|
|
30
|
+
{ id: 'front-right', front: true, side: 1 },
|
|
31
|
+
{ id: 'rear-left', front: false, side: -1 },
|
|
32
|
+
{ id: 'rear-right', front: false, side: 1 },
|
|
33
|
+
].map((wheel) => Object.freeze({
|
|
34
|
+
...wheel,
|
|
35
|
+
x: simUnitsToMeters(VEHICLE_GEOMETRY.wheelLongitudinalOffset) * (wheel.front ? 1 : -1),
|
|
36
|
+
y: simUnitsToMeters(VEHICLE_GEOMETRY.wheelLateralOffset) * wheel.side,
|
|
37
|
+
})));
|
|
38
|
+
|
|
39
|
+
const SURFACES = {
|
|
40
|
+
track: { grip: 1, rolling: 0.012 },
|
|
41
|
+
'pit-entry': { grip: 0.97, rolling: 0.015 },
|
|
42
|
+
'pit-lane': { grip: 0.97, rolling: 0.015 },
|
|
43
|
+
'pit-exit': { grip: 0.97, rolling: 0.015 },
|
|
44
|
+
'pit-box': { grip: 0.94, rolling: 0.02 },
|
|
45
|
+
kerb: { grip: 0.82, rolling: 0.035 },
|
|
46
|
+
gravel: { grip: 0.38, rolling: 0.19 },
|
|
47
|
+
grass: { grip: 0.3, rolling: 0.1 },
|
|
48
|
+
barrier: { grip: 0.2, rolling: 0.25 },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function advancedWheelSurface(car, wheelId) {
|
|
52
|
+
const name = car.wheelStates?.find((wheel) => wheel.id === wheelId)?.surface ?? car.trackState?.surface;
|
|
53
|
+
return SURFACES[name] ?? SURFACES.track;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Requested longitudinal drive force at full throttle, before tire adhesion.
|
|
57
|
+
export function advancedDriveForce(car, forwardSpeedMps) {
|
|
58
|
+
return car.powerNewtons * ADVANCED_VEHICLE_MODEL.engineForceScale * Math.min(1, ADVANCED_VEHICLE_MODEL.enginePowerSpeed / Math.max(Math.abs(forwardSpeedMps), ADVANCED_VEHICLE_MODEL.enginePowerSpeed));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Drag force is this coefficient times speed squared. The integrator applies
|
|
62
|
+
// it opposite the velocity vector; pace control estimates straight resistance.
|
|
63
|
+
export function advancedDragFactor(car) {
|
|
64
|
+
return 0.5 * ADVANCED_VEHICLE_MODEL.airDensity * car.dragCoefficient * ADVANCED_VEHICLE_MODEL.dragAreaScale * (car.drsActive ? 0.72 : 1);
|
|
65
|
+
}
|