@inventure71/paddockjs 0.3.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 +91 -0
- package/INSTALL_AND_UPDATE.md +5 -2
- package/README.md +328 -13
- package/docs/architecture.md +150 -41
- package/docs/concepts.md +22 -3
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +474 -34
- package/docs/data_model.md +1 -1
- package/docs/index.md +1 -0
- package/docs/learnings.md +23 -1
- package/docs/rules.md +169 -21
- package/docs/system_specs.md +56 -20
- package/docs/training.md +369 -25
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +15 -4
- package/src/api/PaddockSimulatorController.js +36 -2
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +615 -940
- 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 +14 -1
- 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 +24 -4
- package/src/environment/actions.js +38 -7
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +206 -0
- package/src/environment/index.d.ts +638 -6
- package/src/environment/index.js +14 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +33 -0
- package/src/environment/observations.js +500 -43
- package/src/environment/options.js +41 -2
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +260 -24
- package/src/environment/scenarios.js +249 -0
- 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 -257
- package/src/environment/specs.js +188 -9
- package/src/environment/workerProtocol.js +64 -0
- package/src/index.d.ts +533 -7
- package/src/index.js +26 -0
- package/src/rendering/proceduralTrackAsset.js +20 -510
- 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 +214 -0
- 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 -253
- 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 +386 -1012
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/collisionSteward.js +69 -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/penaltyLedger.js +153 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -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/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +34 -0
- 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 -628
- package/src/simulation/units.js +6 -4
- 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 -0
- package/src/simulation/vehiclePhysics.js +1 -104
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -0
- package/src/styles.css +342 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +20 -426
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +6 -5
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/templateUtils.js +23 -0
- package/src/ui/timingTowerTemplate.js +34 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { normalizeAngle } from '../simulation/simMath.js';
|
|
2
|
+
import { offsetTrackPoint, pointAt } from '../simulation/track/trackModel.js';
|
|
3
|
+
import { kphToSimSpeed, metersToSimUnits, simUnitsToMeters } from '../simulation/units.js';
|
|
4
|
+
|
|
5
|
+
export const ENVIRONMENT_SCENARIO_PRESETS = Object.freeze([
|
|
6
|
+
'cornering',
|
|
7
|
+
'off-track-recovery',
|
|
8
|
+
'overtaking-pack',
|
|
9
|
+
'pit-entry',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export function resolveScenarioPlacementConfig(scenario = {}, driverIds = new Set()) {
|
|
13
|
+
const preset = scenario.preset == null ? null : String(scenario.preset);
|
|
14
|
+
if (preset && !ENVIRONMENT_SCENARIO_PRESETS.includes(preset)) {
|
|
15
|
+
throw new Error(`PaddockJS environment scenario.preset must be one of: ${ENVIRONMENT_SCENARIO_PRESETS.join(', ')}.`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const placements = normalizePlacementMap(scenario.placements, driverIds);
|
|
19
|
+
const traffic = normalizeTrafficLayout(scenario.traffic, driverIds);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
preset,
|
|
23
|
+
placements,
|
|
24
|
+
traffic,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function applyEnvironmentScenario(sim, options) {
|
|
29
|
+
const scenario = options.scenario ?? {};
|
|
30
|
+
if (!scenario.preset && Object.keys(scenario.placements ?? {}).length === 0 && (scenario.traffic ?? []).length === 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const snapshot = sim.snapshot();
|
|
35
|
+
const carsById = new Map(snapshot.cars.map((car) => [car.id, car]));
|
|
36
|
+
const placements = {
|
|
37
|
+
...buildPresetPlacements(snapshot, options),
|
|
38
|
+
...(scenario.placements ?? {}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
applyEnvironmentPlacements(sim, snapshot.track, carsById, placements);
|
|
42
|
+
|
|
43
|
+
const nextSnapshot = sim.snapshot();
|
|
44
|
+
const nextCarsById = new Map(nextSnapshot.cars.map((car) => [car.id, car]));
|
|
45
|
+
scenario.traffic?.forEach((trafficPlacement) => {
|
|
46
|
+
const reference = nextCarsById.get(trafficPlacement.relativeTo);
|
|
47
|
+
const car = nextCarsById.get(trafficPlacement.driverId);
|
|
48
|
+
if (!reference || !car) return;
|
|
49
|
+
applyEnvironmentPlacements(sim, nextSnapshot.track, nextCarsById, {
|
|
50
|
+
[trafficPlacement.driverId]: {
|
|
51
|
+
distanceMeters: reference.distanceMeters + trafficPlacement.deltaDistanceMeters,
|
|
52
|
+
offsetMeters: trafficPlacement.offsetMeters,
|
|
53
|
+
speedKph: trafficPlacement.speedKph ?? reference.speedKph,
|
|
54
|
+
headingErrorRadians: trafficPlacement.headingErrorRadians ?? 0,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeEnvironmentPlacements(placements = {}, driverIds = new Set()) {
|
|
61
|
+
return normalizePlacementMap(placements, driverIds);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizePlacementMap(value, driverIds) {
|
|
65
|
+
if (value == null) return {};
|
|
66
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
67
|
+
throw new Error('PaddockJS environment scenario.placements must be an object keyed by driver id.');
|
|
68
|
+
}
|
|
69
|
+
return Object.fromEntries(Object.entries(value).map(([driverId, placement]) => {
|
|
70
|
+
assertKnownDriver(driverId, driverIds, 'scenario placement');
|
|
71
|
+
return [driverId, normalizePlacement(placement, `scenario.placements.${driverId}`)];
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeTrafficLayout(value, driverIds) {
|
|
76
|
+
if (value == null) return [];
|
|
77
|
+
if (!Array.isArray(value)) {
|
|
78
|
+
throw new Error('PaddockJS environment scenario.traffic must be an array.');
|
|
79
|
+
}
|
|
80
|
+
return value.map((entry, index) => {
|
|
81
|
+
if (!entry || typeof entry !== 'object') {
|
|
82
|
+
throw new Error(`PaddockJS environment scenario.traffic[${index}] must be an object.`);
|
|
83
|
+
}
|
|
84
|
+
assertKnownDriver(entry.driverId, driverIds, `scenario.traffic[${index}].driverId`);
|
|
85
|
+
assertKnownDriver(entry.relativeTo, driverIds, `scenario.traffic[${index}].relativeTo`);
|
|
86
|
+
return {
|
|
87
|
+
driverId: entry.driverId,
|
|
88
|
+
relativeTo: entry.relativeTo,
|
|
89
|
+
deltaDistanceMeters: finiteNumber(entry.deltaDistanceMeters, 0, `scenario.traffic[${index}].deltaDistanceMeters`),
|
|
90
|
+
offsetMeters: finiteNumber(entry.offsetMeters, 0, `scenario.traffic[${index}].offsetMeters`),
|
|
91
|
+
speedKph: optionalFiniteNumber(entry.speedKph, `scenario.traffic[${index}].speedKph`),
|
|
92
|
+
headingErrorRadians: finiteNumber(entry.headingErrorRadians, 0, `scenario.traffic[${index}].headingErrorRadians`),
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizePlacement(placement, label) {
|
|
98
|
+
if (!placement || typeof placement !== 'object') {
|
|
99
|
+
throw new Error(`PaddockJS environment ${label} must be an object.`);
|
|
100
|
+
}
|
|
101
|
+
const distanceMeters = placement.distanceMeters ?? placement.startDistanceMeters;
|
|
102
|
+
return {
|
|
103
|
+
distanceMeters: optionalFiniteNumber(distanceMeters, `${label}.distanceMeters`),
|
|
104
|
+
offsetMeters: finiteNumber(placement.offsetMeters, 0, `${label}.offsetMeters`),
|
|
105
|
+
speedKph: optionalFiniteNumber(placement.speedKph, `${label}.speedKph`),
|
|
106
|
+
headingErrorRadians: finiteNumber(placement.headingErrorRadians, 0, `${label}.headingErrorRadians`),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildPresetPlacements(snapshot, options) {
|
|
111
|
+
const preset = options.scenario?.preset;
|
|
112
|
+
if (!preset) return {};
|
|
113
|
+
const controlledDriverId = options.controlledDrivers[0];
|
|
114
|
+
const cars = snapshot.cars;
|
|
115
|
+
const track = snapshot.track;
|
|
116
|
+
const primaryDistance = presetBaseDistance(track, preset);
|
|
117
|
+
const placements = {};
|
|
118
|
+
|
|
119
|
+
if (preset === 'cornering') {
|
|
120
|
+
placements[controlledDriverId] = {
|
|
121
|
+
distanceMeters: simUnitsToMeters(primaryDistance),
|
|
122
|
+
offsetMeters: 0,
|
|
123
|
+
speedKph: 135,
|
|
124
|
+
headingErrorRadians: 0,
|
|
125
|
+
};
|
|
126
|
+
} else if (preset === 'off-track-recovery') {
|
|
127
|
+
placements[controlledDriverId] = {
|
|
128
|
+
distanceMeters: simUnitsToMeters(primaryDistance),
|
|
129
|
+
offsetMeters: simUnitsToMeters(track.width / 2) + 7,
|
|
130
|
+
speedKph: 52,
|
|
131
|
+
headingErrorRadians: -0.45,
|
|
132
|
+
};
|
|
133
|
+
} else if (preset === 'pit-entry') {
|
|
134
|
+
placements[controlledDriverId] = {
|
|
135
|
+
distanceMeters: simUnitsToMeters(primaryDistance),
|
|
136
|
+
offsetMeters: 0,
|
|
137
|
+
speedKph: 120,
|
|
138
|
+
headingErrorRadians: 0,
|
|
139
|
+
};
|
|
140
|
+
} else if (preset === 'overtaking-pack') {
|
|
141
|
+
const baseMeters = simUnitsToMeters(primaryDistance);
|
|
142
|
+
cars.slice(0, 4).forEach((car, index) => {
|
|
143
|
+
placements[car.id] = {
|
|
144
|
+
distanceMeters: baseMeters + (index - 1) * 18,
|
|
145
|
+
offsetMeters: index % 2 === 0 ? -2.2 : 2.2,
|
|
146
|
+
speedKph: 135 - index * 3,
|
|
147
|
+
headingErrorRadians: 0,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return placements;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function presetBaseDistance(track, preset) {
|
|
156
|
+
if (preset === 'cornering') return findHighCurvatureDistance(track) - metersToSimUnits(70);
|
|
157
|
+
if (preset === 'off-track-recovery') return track.length * 0.42;
|
|
158
|
+
if (preset === 'overtaking-pack') return track.length * 0.35;
|
|
159
|
+
if (preset === 'pit-entry') {
|
|
160
|
+
const pitDistance = track.pitLane?.entry?.trackDistance ??
|
|
161
|
+
track.pitLane?.entry?.distanceFromStart ??
|
|
162
|
+
track.length * 0.92;
|
|
163
|
+
return pitDistance - metersToSimUnits(170);
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function findHighCurvatureDistance(track) {
|
|
169
|
+
const samples = Array.isArray(track.samples) ? track.samples : [];
|
|
170
|
+
const fallback = track.length * 0.25;
|
|
171
|
+
const best = samples
|
|
172
|
+
.filter((sample) => sample.distance > metersToSimUnits(350))
|
|
173
|
+
.reduce((selected, sample) => {
|
|
174
|
+
if (!selected || (sample.curvature ?? 0) > (selected.curvature ?? 0)) return sample;
|
|
175
|
+
return selected;
|
|
176
|
+
}, null);
|
|
177
|
+
return best?.distance ?? fallback;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function applyEnvironmentPlacements(sim, track, carsById, placements = {}) {
|
|
181
|
+
const states = {};
|
|
182
|
+
Object.entries(placements).forEach(([driverId, placement]) => {
|
|
183
|
+
const currentCar = carsById.get(driverId);
|
|
184
|
+
if (!currentCar) return;
|
|
185
|
+
states[driverId] = placementToCarState(track, currentCar, placement);
|
|
186
|
+
});
|
|
187
|
+
if (typeof sim.setCarStates === 'function') {
|
|
188
|
+
sim.setCarStates(states);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
Object.entries(states).forEach(([driverId, partial]) => {
|
|
192
|
+
sim.setCarState(driverId, partial);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function placementToCarState(track, currentCar, placement) {
|
|
197
|
+
const currentDistanceMeters = currentCar.distanceMeters ?? simUnitsToMeters(currentCar.raceDistance ?? currentCar.progress ?? 0);
|
|
198
|
+
const raceDistance = metersToSimUnits(Math.max(0, placement.distanceMeters ?? currentDistanceMeters));
|
|
199
|
+
const offset = metersToSimUnits(placement.offsetMeters ?? 0);
|
|
200
|
+
const base = pointAt(track, raceDistance);
|
|
201
|
+
const position = offsetTrackPoint(base, offset);
|
|
202
|
+
const heading = normalizeAngle(base.heading + (placement.headingErrorRadians ?? 0));
|
|
203
|
+
const partial = {
|
|
204
|
+
x: position.x,
|
|
205
|
+
y: position.y,
|
|
206
|
+
previousX: position.x,
|
|
207
|
+
previousY: position.y,
|
|
208
|
+
progress: base.distance,
|
|
209
|
+
raceDistance,
|
|
210
|
+
desiredOffset: offset,
|
|
211
|
+
heading,
|
|
212
|
+
previousHeading: heading,
|
|
213
|
+
destroyed: false,
|
|
214
|
+
destroyReason: null,
|
|
215
|
+
destroyedAt: null,
|
|
216
|
+
outOfRace: false,
|
|
217
|
+
canAttack: true,
|
|
218
|
+
throttle: 0,
|
|
219
|
+
brake: 0,
|
|
220
|
+
steeringAngle: 0,
|
|
221
|
+
yawRate: 0,
|
|
222
|
+
stabilityState: 'stable',
|
|
223
|
+
};
|
|
224
|
+
if (placement.speedKph != null) partial.speed = kphToSimSpeed(placement.speedKph);
|
|
225
|
+
return partial;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function assertKnownDriver(driverId, driverIds, label) {
|
|
229
|
+
if (typeof driverId !== 'string' || driverId.length === 0) {
|
|
230
|
+
throw new Error(`PaddockJS environment ${label} must be a driver id.`);
|
|
231
|
+
}
|
|
232
|
+
if (!driverIds.has(driverId)) {
|
|
233
|
+
throw new Error(`PaddockJS environment ${label} does not exist: ${driverId}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function finiteNumber(value, fallback, label) {
|
|
238
|
+
if (value == null) return fallback;
|
|
239
|
+
const number = Number(value);
|
|
240
|
+
if (!Number.isFinite(number)) {
|
|
241
|
+
throw new Error(`PaddockJS environment ${label} must be a finite number.`);
|
|
242
|
+
}
|
|
243
|
+
return number;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function optionalFiniteNumber(value, label) {
|
|
247
|
+
if (value == null) return undefined;
|
|
248
|
+
return finiteNumber(value, undefined, label);
|
|
249
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { simSpeedToMetersPerSecond } from '../../simulation/units.js';
|
|
2
|
+
|
|
3
|
+
export function buildBodySenses(car) {
|
|
4
|
+
return {
|
|
5
|
+
id: car.id,
|
|
6
|
+
speedKph: car.speedKph,
|
|
7
|
+
speedMetersPerSecond: simSpeedToMetersPerSecond(car.speed ?? 0),
|
|
8
|
+
headingRadians: car.heading,
|
|
9
|
+
yawRateRadiansPerSecond: car.yawRate ?? 0,
|
|
10
|
+
steeringAngleRadians: car.steeringAngle ?? 0,
|
|
11
|
+
throttle: car.throttle ?? 0,
|
|
12
|
+
brake: car.brake ?? 0,
|
|
13
|
+
appliedControls: car.appliedControls ?? null,
|
|
14
|
+
lateralG: car.lateralG ?? 0,
|
|
15
|
+
longitudinalG: car.longitudinalG ?? 0,
|
|
16
|
+
gripUsage: car.gripUsage ?? 0,
|
|
17
|
+
slipAngleRadians: car.slipAngleRadians ?? 0,
|
|
18
|
+
tractionLimited: Boolean(car.tractionLimited),
|
|
19
|
+
stabilityState: car.stabilityState ?? 'stable',
|
|
20
|
+
destroyed: Boolean(car.destroyed),
|
|
21
|
+
destroyReason: car.destroyReason ?? null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { simUnitsToMeters } from '../../simulation/units.js';
|
|
2
|
+
|
|
3
|
+
export function buildBoundarySenses(car, snapshot, onLegalSurface) {
|
|
4
|
+
const widthMeters = simUnitsToMeters(snapshot.track?.width ?? 0);
|
|
5
|
+
const halfWidthMeters = widthMeters / 2;
|
|
6
|
+
const lateralOffsetMeters = simUnitsToMeters(car.signedOffset ?? 0);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
lateralOffsetMeters,
|
|
10
|
+
headingErrorRadians: car.trackHeadingError ?? 0,
|
|
11
|
+
legalWidthMeters: widthMeters,
|
|
12
|
+
leftBoundaryMeters: halfWidthMeters + lateralOffsetMeters,
|
|
13
|
+
rightBoundaryMeters: halfWidthMeters - lateralOffsetMeters,
|
|
14
|
+
onLegalSurface,
|
|
15
|
+
surface: car.surface ?? 'track',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
|
|
2
|
+
import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
|
|
3
|
+
import { dot, getCarRayOrigin, getCarRayVector, intersectAxisAlignedBoxRay } from './rayGeometry.js';
|
|
4
|
+
import { rayDetectableTargets } from './sensorTargets.js';
|
|
5
|
+
|
|
6
|
+
export function estimateCarHit(
|
|
7
|
+
car,
|
|
8
|
+
snapshot,
|
|
9
|
+
angleDegrees,
|
|
10
|
+
lengthMeters,
|
|
11
|
+
origin = getCarRayOrigin(car),
|
|
12
|
+
targets = rayDetectableTargets(car, snapshot),
|
|
13
|
+
) {
|
|
14
|
+
const ray = getCarRayVector(car, angleDegrees);
|
|
15
|
+
const maxDistance = metersToSimUnits(lengthMeters);
|
|
16
|
+
let closest = null;
|
|
17
|
+
targets.forEach((other) => {
|
|
18
|
+
if (!carRayBroadphaseHit(origin, ray, maxDistance, other)) return;
|
|
19
|
+
const hitDistance = intersectCarFootprint(origin, ray, other);
|
|
20
|
+
if (hitDistance == null || hitDistance > maxDistance) return;
|
|
21
|
+
const distanceMeters = simUnitsToMeters(hitDistance);
|
|
22
|
+
if (!closest || hitDistance < closest.distanceSimUnits) {
|
|
23
|
+
closest = {
|
|
24
|
+
hit: true,
|
|
25
|
+
distanceSimUnits: hitDistance,
|
|
26
|
+
distanceMeters,
|
|
27
|
+
driverId: other.id,
|
|
28
|
+
targetId: other.id,
|
|
29
|
+
targetType: other.entityType,
|
|
30
|
+
relativeSpeedKph: other.speedKph - car.speedKph,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (!closest) return createCarRayMiss(lengthMeters);
|
|
35
|
+
const { distanceSimUnits, ...publicHit } = closest;
|
|
36
|
+
return publicHit;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createCarRayMiss(lengthMeters) {
|
|
40
|
+
return {
|
|
41
|
+
hit: false,
|
|
42
|
+
distanceMeters: lengthMeters,
|
|
43
|
+
driverId: null,
|
|
44
|
+
targetId: null,
|
|
45
|
+
targetType: null,
|
|
46
|
+
relativeSpeedKph: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function carRayBroadphaseHit(origin, ray, maxDistance, other) {
|
|
51
|
+
const dx = other.x - origin.x;
|
|
52
|
+
const dy = other.y - origin.y;
|
|
53
|
+
const projection = dx * ray.x + dy * ray.y;
|
|
54
|
+
const radius = Math.hypot(VEHICLE_GEOMETRY.bodyLength, VEHICLE_GEOMETRY.bodyWidth) / 2;
|
|
55
|
+
if (projection < -radius || projection > maxDistance + radius) return false;
|
|
56
|
+
const perpendicularSquared = Math.max(0, dx * dx + dy * dy - projection * projection);
|
|
57
|
+
return perpendicularSquared <= radius * radius;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function intersectCarFootprint(origin, ray, other) {
|
|
61
|
+
const forward = { x: Math.cos(other.heading), y: Math.sin(other.heading) };
|
|
62
|
+
const right = { x: -Math.sin(other.heading), y: Math.cos(other.heading) };
|
|
63
|
+
const delta = { x: origin.x - other.x, y: origin.y - other.y };
|
|
64
|
+
const localOrigin = {
|
|
65
|
+
x: dot(delta, forward),
|
|
66
|
+
y: dot(delta, right),
|
|
67
|
+
};
|
|
68
|
+
const localRay = {
|
|
69
|
+
x: dot(ray, forward),
|
|
70
|
+
y: dot(ray, right),
|
|
71
|
+
};
|
|
72
|
+
const halfLength = VEHICLE_GEOMETRY.bodyLength / 2;
|
|
73
|
+
const halfWidth = VEHICLE_GEOMETRY.bodyWidth / 2;
|
|
74
|
+
return intersectAxisAlignedBoxRay(localOrigin, localRay, halfLength, halfWidth);
|
|
75
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { simUnitsToMeters } from '../../simulation/units.js';
|
|
2
|
+
|
|
3
|
+
const SURFACE_CODES = {
|
|
4
|
+
track: 0,
|
|
5
|
+
kerb: 1,
|
|
6
|
+
'pit-entry': 2,
|
|
7
|
+
'pit-lane': 2,
|
|
8
|
+
'pit-exit': 2,
|
|
9
|
+
'pit-box': 2,
|
|
10
|
+
grass: 3,
|
|
11
|
+
gravel: 4,
|
|
12
|
+
barrier: 5,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const CONTACT_PATCH_IDS = Object.freeze(['front-left', 'front-right', 'rear-left', 'rear-right']);
|
|
16
|
+
|
|
17
|
+
export function buildContactPatchSenses(car) {
|
|
18
|
+
const wheelsById = new Map((car.wheels ?? []).map((wheel) => [wheel.id, wheel]));
|
|
19
|
+
return CONTACT_PATCH_IDS.map((id) => {
|
|
20
|
+
const wheel = wheelsById.get(id);
|
|
21
|
+
return {
|
|
22
|
+
id,
|
|
23
|
+
present: Boolean(wheel),
|
|
24
|
+
signedOffsetMeters: simUnitsToMeters(wheel?.signedOffset ?? 0),
|
|
25
|
+
crossTrackErrorMeters: simUnitsToMeters(wheel?.crossTrackError ?? 0),
|
|
26
|
+
surface: wheel?.surface ?? 'track',
|
|
27
|
+
surfaceCode: SURFACE_CODES[wheel?.surface ?? 'track'] ?? SURFACE_CODES.barrier,
|
|
28
|
+
onLegalSurface: Boolean(wheel?.onTrack || wheel?.inPitLane),
|
|
29
|
+
inPitLane: Boolean(wheel?.inPitLane),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createCarRayMiss, estimateCarHit } from './carRays.js';
|
|
2
|
+
import { normalizeRayOptions } from './rayConfig.js';
|
|
3
|
+
import { degreesToRadians, getCarRayOrigin } from './rayGeometry.js';
|
|
4
|
+
import { rayDetectableTargetsForSnapshot } from './sensorTargets.js';
|
|
5
|
+
import { createTrackMiss, createTrackRayContext, estimateTrackHit } from './trackRays.js';
|
|
6
|
+
import { createSurfaceMiss, estimateSurfaceHits, requestedSurfaceChannels } from './surfaceRays.js';
|
|
7
|
+
import { canUseBatchTrainingRayApproximation } from './rayGuards.js';
|
|
8
|
+
|
|
9
|
+
export { buildNearbyCars } from './nearbyCars.js';
|
|
10
|
+
export { DEFAULT_RAY_ANGLES_DEGREES } from './rayDefaults.js';
|
|
11
|
+
export { normalizeRayOptions, RAY_CHANNELS, RAY_LAYOUT_PRESETS } from './rayConfig.js';
|
|
12
|
+
export { getCarRayOrigin, getCarRayVector } from './rayGeometry.js';
|
|
13
|
+
|
|
14
|
+
export function createRayBatchContext(snapshot) {
|
|
15
|
+
return {
|
|
16
|
+
rayTargets: rayDetectableTargetsForSnapshot(snapshot),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildRaySensors(car, snapshot, rayOptions = {}, batchContext = null) {
|
|
21
|
+
const normalized = normalizeRayOptions(rayOptions);
|
|
22
|
+
if (!normalized.enabled) return [];
|
|
23
|
+
if (car?.destroyed || car?.outOfRace) {
|
|
24
|
+
return buildInactiveCarRays(normalized);
|
|
25
|
+
}
|
|
26
|
+
const origin = getCarRayOrigin(car);
|
|
27
|
+
const usesTrackContext = normalized.channels.includes('roadEdge') ||
|
|
28
|
+
requestedSurfaceChannels(normalized.channels).length > 0;
|
|
29
|
+
const trackContext = !usesTrackContext
|
|
30
|
+
? null
|
|
31
|
+
: createTrackRayContext(car, snapshot, origin);
|
|
32
|
+
const carTargets = normalized.channels.includes('car')
|
|
33
|
+
? (batchContext?.rayTargets ?? rayDetectableTargetsForSnapshot(snapshot)).filter((target) => target.id !== car.id)
|
|
34
|
+
: [];
|
|
35
|
+
|
|
36
|
+
if (canUseFastBatchTrainingRays(car, snapshot, trackContext)) {
|
|
37
|
+
return buildFastBatchTrainingRays(car, snapshot, normalized, origin, carTargets, trackContext);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return normalized.rays.map((ray) => {
|
|
41
|
+
const angleDegrees = ray.angleDegrees;
|
|
42
|
+
const vector = {
|
|
43
|
+
x: Math.cos((car.heading ?? 0) + degreesToRadians(angleDegrees)),
|
|
44
|
+
y: Math.sin((car.heading ?? 0) + degreesToRadians(angleDegrees)),
|
|
45
|
+
};
|
|
46
|
+
const sharedRayQuery = {};
|
|
47
|
+
const roadEdge = normalized.channels.includes('roadEdge')
|
|
48
|
+
? estimateTrackHit(
|
|
49
|
+
car,
|
|
50
|
+
snapshot,
|
|
51
|
+
angleDegrees,
|
|
52
|
+
ray.lengthMeters,
|
|
53
|
+
trackContext,
|
|
54
|
+
{ precision: normalized.precision, sharedRayQuery },
|
|
55
|
+
)
|
|
56
|
+
: createTrackMiss(ray.lengthMeters);
|
|
57
|
+
const carHit = normalized.channels.includes('car')
|
|
58
|
+
? estimateCarHit(car, snapshot, angleDegrees, ray.lengthMeters, origin, carTargets)
|
|
59
|
+
: createCarRayMiss(ray.lengthMeters);
|
|
60
|
+
const surfaceHits = estimateSurfaceHits(
|
|
61
|
+
car,
|
|
62
|
+
snapshot,
|
|
63
|
+
ray,
|
|
64
|
+
origin,
|
|
65
|
+
vector,
|
|
66
|
+
normalized.channels,
|
|
67
|
+
trackContext,
|
|
68
|
+
{ precision: normalized.precision, sharedRayQuery },
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: ray.id,
|
|
73
|
+
angleDegrees,
|
|
74
|
+
angleRadians: degreesToRadians(angleDegrees),
|
|
75
|
+
lengthMeters: ray.lengthMeters,
|
|
76
|
+
roadEdge,
|
|
77
|
+
track: roadEdge,
|
|
78
|
+
kerb: surfaceHits.kerb ?? createSurfaceMiss(ray.lengthMeters),
|
|
79
|
+
illegalSurface: surfaceHits.illegalSurface ?? createSurfaceMiss(ray.lengthMeters),
|
|
80
|
+
car: carHit,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildInactiveCarRays(normalized) {
|
|
86
|
+
return normalized.rays.map((ray) => ({
|
|
87
|
+
id: ray.id,
|
|
88
|
+
angleDegrees: ray.angleDegrees,
|
|
89
|
+
angleRadians: degreesToRadians(ray.angleDegrees),
|
|
90
|
+
lengthMeters: ray.lengthMeters,
|
|
91
|
+
roadEdge: createTrackMiss(ray.lengthMeters),
|
|
92
|
+
track: createTrackMiss(ray.lengthMeters),
|
|
93
|
+
kerb: createSurfaceMiss(ray.lengthMeters),
|
|
94
|
+
illegalSurface: createSurfaceMiss(ray.lengthMeters),
|
|
95
|
+
car: createCarRayMiss(ray.lengthMeters),
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function canUseFastBatchTrainingRays(car, snapshot, trackContext) {
|
|
100
|
+
return car?.interaction?.profile === 'batch-training' &&
|
|
101
|
+
!car.inPitLane &&
|
|
102
|
+
!trackContext?.originState?.inPitLane &&
|
|
103
|
+
Array.isArray(snapshot.track?.samples) &&
|
|
104
|
+
snapshot.track.samples.length > 0 &&
|
|
105
|
+
trackContext?.originState &&
|
|
106
|
+
canUseBatchTrainingRayApproximation(snapshot.track, trackContext.originState);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildFastBatchTrainingRays(car, snapshot, normalized, origin, carTargets, trackContext) {
|
|
110
|
+
const channels = new Set(normalized.channels);
|
|
111
|
+
|
|
112
|
+
return normalized.rays.map((ray) => {
|
|
113
|
+
const angleRadians = degreesToRadians(ray.angleDegrees);
|
|
114
|
+
const heading = (car.heading ?? 0) + angleRadians;
|
|
115
|
+
const vector = { x: Math.cos(heading), y: Math.sin(heading) };
|
|
116
|
+
const sharedRayQuery = {};
|
|
117
|
+
const roadEdge = channels.has('roadEdge')
|
|
118
|
+
? estimateTrackHit(
|
|
119
|
+
car,
|
|
120
|
+
snapshot,
|
|
121
|
+
ray.angleDegrees,
|
|
122
|
+
ray.lengthMeters,
|
|
123
|
+
trackContext,
|
|
124
|
+
{ precision: normalized.precision, sharedRayQuery },
|
|
125
|
+
)
|
|
126
|
+
: createTrackMiss(ray.lengthMeters);
|
|
127
|
+
const carHit = channels.has('car') && carTargets.length > 0
|
|
128
|
+
? estimateCarHit(car, snapshot, ray.angleDegrees, ray.lengthMeters, origin, carTargets)
|
|
129
|
+
: createCarRayMiss(ray.lengthMeters);
|
|
130
|
+
const surfaceHits = estimateSurfaceHits(
|
|
131
|
+
car,
|
|
132
|
+
snapshot,
|
|
133
|
+
ray,
|
|
134
|
+
origin,
|
|
135
|
+
vector,
|
|
136
|
+
normalized.channels,
|
|
137
|
+
trackContext,
|
|
138
|
+
{ precision: normalized.precision, sharedRayQuery },
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
id: ray.id,
|
|
143
|
+
angleDegrees: ray.angleDegrees,
|
|
144
|
+
angleRadians,
|
|
145
|
+
lengthMeters: ray.lengthMeters,
|
|
146
|
+
roadEdge,
|
|
147
|
+
track: roadEdge,
|
|
148
|
+
kerb: channels.has('kerb')
|
|
149
|
+
? (surfaceHits.kerb ?? createSurfaceMiss(ray.lengthMeters))
|
|
150
|
+
: createSurfaceMiss(ray.lengthMeters),
|
|
151
|
+
illegalSurface: channels.has('illegalSurface')
|
|
152
|
+
? (surfaceHits.illegalSurface ?? createSurfaceMiss(ray.lengthMeters))
|
|
153
|
+
: createSurfaceMiss(ray.lengthMeters),
|
|
154
|
+
car: carHit,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { queryTrackSegmentsAlongRay } from '../../simulation/track/trackQueryIndex.js';
|
|
2
|
+
import { metersToSimUnits } from '../../simulation/units.js';
|
|
3
|
+
|
|
4
|
+
const RAY_BOUND_QUERY_MARGIN_METERS = 18;
|
|
5
|
+
|
|
6
|
+
export function findIndexedRayBoundaryHit(track, origin, vector, lengthMeters, offsets) {
|
|
7
|
+
const boundaries = findIndexedRayBoundaryDistances(track, origin, vector, lengthMeters, offsets);
|
|
8
|
+
if (!boundaries.available) return { available: false, distance: null };
|
|
9
|
+
return {
|
|
10
|
+
available: true,
|
|
11
|
+
distance: minFinite(...boundaries.distances),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function findIndexedRayBoundaryDistances(track, origin, vector, lengthMeters, offsets) {
|
|
16
|
+
const finiteOffsets = offsets.filter(Number.isFinite);
|
|
17
|
+
if (finiteOffsets.length === 0) return { available: true, distances: [] };
|
|
18
|
+
const maxDistance = metersToSimUnits(lengthMeters);
|
|
19
|
+
const maxOffset = Math.max(0, ...finiteOffsets.map((offset) => Math.abs(offset)));
|
|
20
|
+
const margin = maxOffset + metersToSimUnits(RAY_BOUND_QUERY_MARGIN_METERS);
|
|
21
|
+
const segments = queryTrackSegmentsAlongRay(track, origin, vector, maxDistance, margin);
|
|
22
|
+
if (!segments) return { available: false, distances: [] };
|
|
23
|
+
const distances = finiteOffsets.map(() => Infinity);
|
|
24
|
+
|
|
25
|
+
for (const segment of segments) {
|
|
26
|
+
finiteOffsets.forEach((offset, offsetIndex) => {
|
|
27
|
+
const start = offsetSegmentStart(segment, offset);
|
|
28
|
+
const finish = offsetSegmentEnd(segment, offset);
|
|
29
|
+
const distance = raySegmentIntersectionDistance(origin, vector, start, finish, maxDistance);
|
|
30
|
+
if (distance != null && distance < distances[offsetIndex]) distances[offsetIndex] = distance;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
available: true,
|
|
36
|
+
distances: distances.map((distance) => (Number.isFinite(distance) ? distance : null)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function findIndexedTrackBandBoundaries(
|
|
41
|
+
track,
|
|
42
|
+
origin,
|
|
43
|
+
vector,
|
|
44
|
+
lengthMeters,
|
|
45
|
+
trackHalfWidth,
|
|
46
|
+
kerbOuterWidth,
|
|
47
|
+
cache = null,
|
|
48
|
+
) {
|
|
49
|
+
if (cache?.trackBandBoundaries) return cache.trackBandBoundaries;
|
|
50
|
+
const boundaries = findIndexedRayBoundaryDistances(
|
|
51
|
+
track,
|
|
52
|
+
origin,
|
|
53
|
+
vector,
|
|
54
|
+
lengthMeters,
|
|
55
|
+
[trackHalfWidth, -trackHalfWidth, kerbOuterWidth, -kerbOuterWidth],
|
|
56
|
+
);
|
|
57
|
+
const result = {
|
|
58
|
+
available: boundaries.available,
|
|
59
|
+
trackEdgeDistance: minFinite(boundaries.distances[0], boundaries.distances[1]),
|
|
60
|
+
kerbOuterDistance: minFinite(boundaries.distances[2], boundaries.distances[3]),
|
|
61
|
+
};
|
|
62
|
+
if (cache) cache.trackBandBoundaries = result;
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function offsetSegmentStart(segment, offset) {
|
|
67
|
+
return {
|
|
68
|
+
x: segment.startX + segment.normalX * offset,
|
|
69
|
+
y: segment.startY + segment.normalY * offset,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function offsetSegmentEnd(segment, offset) {
|
|
74
|
+
return {
|
|
75
|
+
x: segment.endX + segment.endNormalX * offset,
|
|
76
|
+
y: segment.endY + segment.endNormalY * offset,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function raySegmentIntersectionDistance(origin, ray, start, end, maxDistance) {
|
|
81
|
+
const sx = end.x - start.x;
|
|
82
|
+
const sy = end.y - start.y;
|
|
83
|
+
const denominator = cross(ray.x, ray.y, sx, sy);
|
|
84
|
+
if (Math.abs(denominator) < 1e-9) return null;
|
|
85
|
+
|
|
86
|
+
const ox = start.x - origin.x;
|
|
87
|
+
const oy = start.y - origin.y;
|
|
88
|
+
const rayDistance = cross(ox, oy, sx, sy) / denominator;
|
|
89
|
+
const segmentAmount = cross(ox, oy, ray.x, ray.y) / denominator;
|
|
90
|
+
|
|
91
|
+
if (rayDistance < 0 || rayDistance > maxDistance) return null;
|
|
92
|
+
if (segmentAmount < -1e-6 || segmentAmount > 1 + 1e-6) return null;
|
|
93
|
+
return rayDistance;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cross(ax, ay, bx, by) {
|
|
97
|
+
return ax * by - ay * bx;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function minFinite(...values) {
|
|
101
|
+
const minimum = Math.min(...values.filter((value) => Number.isFinite(value)));
|
|
102
|
+
return Number.isFinite(minimum) ? minimum : null;
|
|
103
|
+
}
|