@inventure71/paddockjs 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +146 -1
- package/INSTALL_AND_UPDATE.md +15 -4
- package/README.md +84 -562
- package/docs/architecture.md +28 -18
- package/docs/composable-layouts.md +77 -0
- package/docs/custom_model_controller.md +4 -4
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +9 -7
- package/docs/getting-started.md +75 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +17 -4
- package/docs/rules.md +6 -2
- package/docs/system_specs.md +49 -6
- package/docs/theming.md +64 -0
- package/docs/training.md +20 -14
- package/docs/troubleshooting.md +76 -0
- package/docs/upgrading-hosts-to-3.md +42 -0
- package/examples/python/README.md +6 -0
- package/examples/python/base_policy_server.py +19 -0
- package/package.json +10 -3
- package/src/api/PaddockSimulatorController.js +78 -22
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +24 -3
- package/src/app/readouts/timingTowerRenderer.js +203 -42
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/config/defaultOptions.js +8 -1
- package/src/config/themeOptions.js +5 -0
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/episode.js +11 -1
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/runtime.js +41 -0
- package/src/environment/sensors/carRays.js +48 -37
- package/src/environment/sensors/index.js +501 -97
- package/src/environment/sensors/indexedRayBands.js +134 -46
- package/src/environment/sensors/rayBandTrace.js +1171 -0
- package/src/environment/sensors/rayChannels.js +108 -0
- package/src/environment/sensors/rayConfig.js +71 -25
- package/src/environment/sensors/rayGeometry.js +23 -17
- package/src/environment/sensors/sensorTargets.js +67 -1
- package/src/environment/sensors/surfaceRays.js +130 -89
- package/src/environment/sensors/trackRays.js +115 -1
- package/src/index.d.ts +43 -0
- package/src/index.js +27 -2
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +77 -42
- package/src/simulation/pit/pitState.js +1 -0
- package/src/simulation/pit/pitTireService.js +7 -3
- package/src/simulation/race/raceFinish.js +25 -8
- package/src/simulation/race/raceOrder.js +189 -32
- package/src/simulation/race/raceProgress.js +129 -37
- package/src/simulation/race/raceSetup.js +2 -0
- package/src/simulation/race/raceStep.js +25 -13
- package/src/simulation/raceSimulation.js +20 -17
- package/src/simulation/rules/collisionSteward.js +40 -23
- package/src/simulation/rules/rulesReview.js +77 -56
- package/src/simulation/snapshots/raceSnapshots.js +37 -0
- package/src/simulation/timing/drsTiming.js +143 -30
- package/src/simulation/timing/gapEstimation.js +25 -3
- package/src/simulation/timing/lapTelemetry.js +115 -36
- package/src/simulation/timing/raceTiming.js +5 -1
- package/src/simulation/timing/sectorPerformance.js +104 -26
- package/src/simulation/timing/timingHistory.js +229 -14
- package/src/simulation/timing/timingLines.js +65 -7
- package/src/simulation/track/pitLaneLayout.js +23 -0
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +67 -11
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1334 -100
- package/src/simulation/track/trackQueryPitIndex.js +138 -14
- package/src/simulation/track/trackQueryScratch.js +7 -0
- package/src/simulation/track/trackQueryStats.js +25 -0
- package/src/simulation/track/trackSnapshotJson.js +139 -0
- package/src/simulation/track/trackStatePolicy.js +249 -1
- package/src/simulation/vehicle/contactResolution.js +96 -59
- package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
- package/src/simulation/vehicle/pitWheelSurface.js +81 -28
- package/src/simulation/vehicle/runoffResponse.js +54 -29
- package/src/simulation/vehicle/vehicleGeometry.js +245 -51
- package/src/simulation/vehicle/vehiclePhysics.js +42 -27
- package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
- package/src/simulation/vehicle/vehicleState.js +6 -0
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/styles.css +497 -497
|
@@ -3,6 +3,53 @@ import { metersToSimUnits, simUnitsToMeters } from '../units.js';
|
|
|
3
3
|
import { pointAt } from '../track/trackModel.js';
|
|
4
4
|
import { TIMING_LINE_HISTORY_LAPS, TIMING_LINE_TARGET_SPACING_METERS } from './timingConstants.js';
|
|
5
5
|
|
|
6
|
+
function setTimingLineCrossingBounds(car, firstLine, lastLine) {
|
|
7
|
+
car.timingLineFirstStored = Number.isInteger(firstLine) ? firstLine : null;
|
|
8
|
+
car.timingLineLastStored = Number.isInteger(lastLine) ? lastLine : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function ensureTimingLineCrossingBounds(car) {
|
|
12
|
+
if (Number.isInteger(car.timingLineFirstStored) && Number.isInteger(car.timingLineLastStored)) {
|
|
13
|
+
return {
|
|
14
|
+
firstLine: car.timingLineFirstStored,
|
|
15
|
+
lastLine: car.timingLineLastStored,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const lineNumbers = Object.keys(car.timingLineCrossings)
|
|
19
|
+
.map((key) => Number(key))
|
|
20
|
+
.filter((value) => Number.isFinite(value))
|
|
21
|
+
.sort((left, right) => left - right);
|
|
22
|
+
const firstLine = lineNumbers[0];
|
|
23
|
+
const lastLine = lineNumbers[lineNumbers.length - 1];
|
|
24
|
+
setTimingLineCrossingBounds(car, firstLine, lastLine);
|
|
25
|
+
return {
|
|
26
|
+
firstLine: car.timingLineFirstStored,
|
|
27
|
+
lastLine: car.timingLineLastStored,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setTimingLineCrossing(car, lineNumber, crossingTime) {
|
|
32
|
+
car.timingLineCrossings[lineNumber] = crossingTime;
|
|
33
|
+
const firstLine = car.timingLineFirstStored;
|
|
34
|
+
const lastLine = car.timingLineLastStored;
|
|
35
|
+
if (!Number.isInteger(firstLine) || lineNumber < firstLine) car.timingLineFirstStored = lineNumber;
|
|
36
|
+
if (!Number.isInteger(lastLine) || lineNumber > lastLine) car.timingLineLastStored = lineNumber;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pruneTimingLineCrossings(car, cutoffLine) {
|
|
40
|
+
const { firstLine, lastLine } = ensureTimingLineCrossingBounds(car);
|
|
41
|
+
if (!Number.isInteger(firstLine) || !Number.isInteger(lastLine) || cutoffLine <= firstLine) return;
|
|
42
|
+
const pruneEnd = Math.min(lastLine, cutoffLine - 1);
|
|
43
|
+
for (let lineNumber = firstLine; lineNumber <= pruneEnd; lineNumber += 1) {
|
|
44
|
+
delete car.timingLineCrossings[lineNumber];
|
|
45
|
+
}
|
|
46
|
+
if (cutoffLine > lastLine) {
|
|
47
|
+
setTimingLineCrossingBounds(car, null, null);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setTimingLineCrossingBounds(car, cutoffLine, lastLine);
|
|
51
|
+
}
|
|
52
|
+
|
|
6
53
|
export function createTimingLines(track) {
|
|
7
54
|
const targetSpacing = metersToSimUnits(TIMING_LINE_TARGET_SPACING_METERS);
|
|
8
55
|
const count = Math.max(1, Math.round(track.length / targetSpacing));
|
|
@@ -36,6 +83,8 @@ export function resetTimingLineCrossings(car, currentTime) {
|
|
|
36
83
|
car.timingLineCrossings = Object.create(null);
|
|
37
84
|
car.timingLineLastUpdatedAt = currentTime;
|
|
38
85
|
car.previousRaceDistanceForTiming = car.raceDistance;
|
|
86
|
+
setTimingLineCrossingBounds(car, null, null);
|
|
87
|
+
car.timingLinePruneCutoff = null;
|
|
39
88
|
}
|
|
40
89
|
|
|
41
90
|
export function recordTimingLineCrossings(car, previousRaceDistance, currentTime, track) {
|
|
@@ -51,6 +100,18 @@ export function recordTimingLineCrossings(car, previousRaceDistance, currentTime
|
|
|
51
100
|
const travelled = currentDistance - previousDistance;
|
|
52
101
|
const previousTime = Number.isFinite(car.timingLineLastUpdatedAt) ? car.timingLineLastUpdatedAt : currentTime;
|
|
53
102
|
const elapsed = Math.max(0, currentTime - previousTime);
|
|
103
|
+
const latestLine = getTimingLineNumber(track, currentDistance);
|
|
104
|
+
const cutoff = latestLine != null
|
|
105
|
+
? latestLine - (track.timingLines.count * TIMING_LINE_HISTORY_LAPS)
|
|
106
|
+
: null;
|
|
107
|
+
|
|
108
|
+
if (Math.abs(travelled) <= 1e-6) {
|
|
109
|
+
car.timingLineLastUpdatedAt = currentTime;
|
|
110
|
+
if (!Number.isFinite(cutoff) || cutoff <= 0 || cutoff === car.timingLinePruneCutoff) return;
|
|
111
|
+
pruneTimingLineCrossings(car, cutoff);
|
|
112
|
+
car.timingLinePruneCutoff = cutoff;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
54
115
|
|
|
55
116
|
if (travelled > 1e-6 && travelled < track.length / 2) {
|
|
56
117
|
const firstLine = Math.floor(Math.max(0, previousDistance) / spacing) + 1;
|
|
@@ -59,17 +120,14 @@ export function recordTimingLineCrossings(car, previousRaceDistance, currentTime
|
|
|
59
120
|
const lineDistance = lineNumber * spacing;
|
|
60
121
|
if (lineDistance <= previousDistance + 1e-6 || lineDistance > currentDistance + 1e-6) continue;
|
|
61
122
|
const ratio = clamp((lineDistance - previousDistance) / travelled, 0, 1);
|
|
62
|
-
car
|
|
123
|
+
setTimingLineCrossing(car, lineNumber, previousTime + ratio * elapsed);
|
|
63
124
|
}
|
|
64
125
|
}
|
|
65
126
|
|
|
66
127
|
car.timingLineLastUpdatedAt = currentTime;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Object.keys(car.timingLineCrossings).forEach((key) => {
|
|
71
|
-
if (Number(key) < cutoff) delete car.timingLineCrossings[key];
|
|
72
|
-
});
|
|
128
|
+
if (Number.isFinite(cutoff) && cutoff > 0 && cutoff !== car.timingLinePruneCutoff) {
|
|
129
|
+
pruneTimingLineCrossings(car, cutoff);
|
|
130
|
+
car.timingLinePruneCutoff = cutoff;
|
|
73
131
|
}
|
|
74
132
|
}
|
|
75
133
|
|
|
@@ -274,6 +274,27 @@ export function createPitLaneBounds(pitLane) {
|
|
|
274
274
|
return createPointBounds(points, padding);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
export function createPitBoxBounds(pitLane) {
|
|
278
|
+
const points = [
|
|
279
|
+
...(pitLane.boxes ?? []).flatMap((box) => box.corners ?? []),
|
|
280
|
+
...(pitLane.serviceAreas ?? []).flatMap((area) => [
|
|
281
|
+
...(area.corners ?? []),
|
|
282
|
+
...(area.queueCorners ?? []),
|
|
283
|
+
]),
|
|
284
|
+
];
|
|
285
|
+
if (!points.length) return null;
|
|
286
|
+
const padding = Math.max(PIT_BOX_DEPTH, PIT_SERVICE_AREA_DEPTH) / 2 + metersToSimUnits(3);
|
|
287
|
+
return createPointBounds(points, padding);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function createPitConnectorBounds(pitLane) {
|
|
291
|
+
const connectorPadding = metersToSimUnits(18);
|
|
292
|
+
return {
|
|
293
|
+
entry: createPointBounds(pitLane.entry?.connector ?? [], connectorPadding),
|
|
294
|
+
exit: createPointBounds(pitLane.exit?.connector ?? [], connectorPadding),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
277
298
|
export function createPitLaneModel(track) {
|
|
278
299
|
const layout = createPitLaneLayout();
|
|
279
300
|
const laneOffset = track.width / 2 + (track.kerbWidth ?? 0) + PIT_LANE_EDGE_GAP + PIT_LANE_WIDTH / 2;
|
|
@@ -395,5 +416,7 @@ export function createPitLaneModel(track) {
|
|
|
395
416
|
return {
|
|
396
417
|
...pitLane,
|
|
397
418
|
bounds: createPitLaneBounds(pitLane),
|
|
419
|
+
boxBounds: createPitBoxBounds(pitLane),
|
|
420
|
+
connectorBounds: createPitConnectorBounds(pitLane),
|
|
398
421
|
};
|
|
399
422
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { clamp, wrapDistance } from '../simMath.js';
|
|
2
2
|
import { PIT_LANE_WIDTH } from './trackConstants.js';
|
|
3
|
-
import { nearestPointOnPolyline, pointInsideBounds, pointIsInsidePolygon } from './trackMath.js';
|
|
3
|
+
import { nearestPointOnPolyline, nearestPointOnPolylineInto, pointInsideBounds, pointIsInsidePolygon } from './trackMath.js';
|
|
4
4
|
import { queryPitBoxCandidates, queryPitRoadSegmentCandidatesByRoute } from './trackQueryIndex.js';
|
|
5
|
+
import { ensureQueryScratch } from './trackQueryScratch.js';
|
|
5
6
|
|
|
6
7
|
export function mapPitDistance(track, startDistance, endDistance, amount) {
|
|
7
8
|
return wrapDistance(startDistance + (endDistance - startDistance) * clamp(amount, 0, 1), track.length);
|
|
@@ -15,9 +16,37 @@ export function createPitRoadState(track, position, pitLane, {
|
|
|
15
16
|
startDistance,
|
|
16
17
|
endDistance,
|
|
17
18
|
segmentCandidates = null,
|
|
19
|
+
projectionTarget = null,
|
|
20
|
+
projectionScratch = null,
|
|
21
|
+
cumulativeDistances = null,
|
|
22
|
+
connectorProjectionStats = false,
|
|
23
|
+
connectorEndpointWindowProjection = false,
|
|
18
24
|
}) {
|
|
25
|
+
if (!Array.isArray(points) || points.length < 2) return null;
|
|
19
26
|
if (Array.isArray(segmentCandidates) && segmentCandidates.length === 0) return null;
|
|
20
|
-
|
|
27
|
+
if (connectorProjectionStats) {
|
|
28
|
+
const stats = track?.queryIndex?.stats;
|
|
29
|
+
if (stats) {
|
|
30
|
+
if (connectorEndpointWindowProjection && Array.isArray(segmentCandidates) && segmentCandidates.length > 0) {
|
|
31
|
+
stats.pitConnectorEndpointWindowProjectionCalls =
|
|
32
|
+
(stats.pitConnectorEndpointWindowProjectionCalls ?? 0) + 1;
|
|
33
|
+
} else if (!Array.isArray(segmentCandidates)) {
|
|
34
|
+
stats.pitConnectorFullRouteProjectionScans =
|
|
35
|
+
(stats.pitConnectorFullRouteProjectionScans ?? 0) + 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (projectionScratch && Array.isArray(segmentCandidates) && segmentCandidates.length > 0 && !Array.isArray(cumulativeDistances)) {
|
|
40
|
+
const stats = track?.queryIndex?.stats;
|
|
41
|
+
if (stats) stats.pitRoadCumulativeDistanceRebuilds = (stats.pitRoadCumulativeDistanceRebuilds ?? 0) + 1;
|
|
42
|
+
}
|
|
43
|
+
const projected = projectionTarget
|
|
44
|
+
? nearestPointOnPolylineInto(projectionTarget, points, position, {
|
|
45
|
+
segmentIndices: segmentCandidates,
|
|
46
|
+
scratch: projectionScratch,
|
|
47
|
+
cumulativeDistances,
|
|
48
|
+
})
|
|
49
|
+
: nearestPointOnPolyline(points, position, segmentCandidates);
|
|
21
50
|
if (!projected || projected.crossTrackError > roadWidth / 2) return null;
|
|
22
51
|
const amount = projected.totalLength > 0 ? projected.distanceAlong / projected.totalLength : 0;
|
|
23
52
|
const distanceAlongTrack = mapPitDistance(track, startDistance, endDistance, amount);
|
|
@@ -94,21 +123,27 @@ export function createPitBoxState(track, position, pitLane, candidateBoxes = nul
|
|
|
94
123
|
};
|
|
95
124
|
}
|
|
96
125
|
|
|
97
|
-
export function nearestPitLaneState(track, position) {
|
|
126
|
+
export function nearestPitLaneState(track, position, progressHint = null) {
|
|
98
127
|
const pitLane = track.pitLane;
|
|
99
128
|
if (!pitLane?.enabled) return null;
|
|
100
129
|
if (!pointInsideBounds(position, pitLane.bounds)) return null;
|
|
130
|
+
const scratch = track?.queryIndex ? ensureQueryScratch(track.queryIndex) : null;
|
|
101
131
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
if (!pitLane.boxBounds || pointInsideBounds(position, pitLane.boxBounds)) {
|
|
133
|
+
const indexedBoxCandidates = queryPitBoxCandidates(track, position, scratch);
|
|
134
|
+
const boxCandidates = indexedBoxCandidates ?? null;
|
|
135
|
+
const boxState = createPitBoxState(track, position, pitLane, boxCandidates);
|
|
136
|
+
if (boxState) return boxState;
|
|
137
|
+
}
|
|
106
138
|
|
|
107
139
|
const laneEntryDistance = pitLane.layout?.entryDistance ?? pitLane.entry.distanceFromStart;
|
|
108
140
|
const laneExitDistance = pitLane.layout?.exitDistance ?? pitLane.exit.distanceFromStart;
|
|
109
|
-
const roadCandidatesByRoute = queryPitRoadSegmentCandidatesByRoute(track, position);
|
|
110
|
-
const
|
|
111
|
-
|
|
141
|
+
const roadCandidatesByRoute = queryPitRoadSegmentCandidatesByRoute(track, position, progressHint, scratch);
|
|
142
|
+
const pitRoutes = track?.queryIndex?.pit?.routes ?? {};
|
|
143
|
+
const projectionTarget = scratch ? (scratch.pitRoadProjection ??= { point: {} }) : null;
|
|
144
|
+
const projectionScratch = scratch ? (scratch.pitRoadProjectionScratch ??= {}) : null;
|
|
145
|
+
let bestState = null;
|
|
146
|
+
bestState = pickPitState(bestState, createPitRoadState(track, position, pitLane, {
|
|
112
147
|
points: pitLane.entry.roadCenterline,
|
|
113
148
|
surface: 'pit-entry',
|
|
114
149
|
part: 'entry',
|
|
@@ -116,8 +151,11 @@ export function nearestPitLaneState(track, position) {
|
|
|
116
151
|
startDistance: pitLane.entry.distanceFromStart,
|
|
117
152
|
endDistance: laneEntryDistance,
|
|
118
153
|
segmentCandidates: roadCandidatesByRoute?.entry ?? null,
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
projectionTarget,
|
|
155
|
+
projectionScratch,
|
|
156
|
+
cumulativeDistances: pitRoutes.entry?.cumulativeDistances ?? null,
|
|
157
|
+
}));
|
|
158
|
+
bestState = pickPitState(bestState, createPitRoadState(track, position, pitLane, {
|
|
121
159
|
points: pitLane.mainLane.points,
|
|
122
160
|
surface: 'pit-lane',
|
|
123
161
|
part: 'fast-lane',
|
|
@@ -125,8 +163,11 @@ export function nearestPitLaneState(track, position) {
|
|
|
125
163
|
startDistance: laneEntryDistance,
|
|
126
164
|
endDistance: laneExitDistance,
|
|
127
165
|
segmentCandidates: roadCandidatesByRoute?.main ?? null,
|
|
128
|
-
|
|
129
|
-
|
|
166
|
+
projectionTarget,
|
|
167
|
+
projectionScratch,
|
|
168
|
+
cumulativeDistances: pitRoutes.main?.cumulativeDistances ?? null,
|
|
169
|
+
}));
|
|
170
|
+
bestState = pickPitState(bestState, createPitRoadState(track, position, pitLane, {
|
|
130
171
|
points: pitLane.workingLane?.points,
|
|
131
172
|
surface: 'pit-lane',
|
|
132
173
|
part: 'working-lane',
|
|
@@ -134,8 +175,11 @@ export function nearestPitLaneState(track, position) {
|
|
|
134
175
|
startDistance: laneEntryDistance,
|
|
135
176
|
endDistance: laneExitDistance,
|
|
136
177
|
segmentCandidates: roadCandidatesByRoute?.working ?? null,
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
projectionTarget,
|
|
179
|
+
projectionScratch,
|
|
180
|
+
cumulativeDistances: pitRoutes.working?.cumulativeDistances ?? null,
|
|
181
|
+
}));
|
|
182
|
+
bestState = pickPitState(bestState, createPitRoadState(track, position, pitLane, {
|
|
139
183
|
points: pitLane.exit.roadCenterline,
|
|
140
184
|
surface: 'pit-exit',
|
|
141
185
|
part: 'exit',
|
|
@@ -143,10 +187,165 @@ export function nearestPitLaneState(track, position) {
|
|
|
143
187
|
startDistance: laneExitDistance,
|
|
144
188
|
endDistance: pitLane.exit.distanceFromStart,
|
|
145
189
|
segmentCandidates: roadCandidatesByRoute?.exit ?? null,
|
|
146
|
-
|
|
147
|
-
|
|
190
|
+
projectionTarget,
|
|
191
|
+
projectionScratch,
|
|
192
|
+
cumulativeDistances: pitRoutes.exit?.cumulativeDistances ?? null,
|
|
193
|
+
}));
|
|
148
194
|
|
|
149
|
-
return
|
|
195
|
+
return bestState;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resolveDirectConnectorPitLaneState(track, position, progressHint = null) {
|
|
199
|
+
const pitLane = track?.pitLane;
|
|
200
|
+
if (!pitLane?.enabled) return undefined;
|
|
201
|
+
const inEntryConnector = pointInsideBounds(position, pitLane.connectorBounds?.entry);
|
|
202
|
+
const inExitConnector = pointInsideBounds(position, pitLane.connectorBounds?.exit);
|
|
203
|
+
const nearEntryProgress = Number.isFinite(progressHint) && progressHint >= pitLane.entry.trackDistance;
|
|
204
|
+
const nearExitProgress = Number.isFinite(progressHint) &&
|
|
205
|
+
progressHint >= 0 &&
|
|
206
|
+
progressHint <= pitLane.exit.trackDistance;
|
|
207
|
+
if (!inEntryConnector && !inExitConnector && !nearEntryProgress && !nearExitProgress) return undefined;
|
|
208
|
+
|
|
209
|
+
const laneEntryDistance = pitLane.layout?.entryDistance ?? pitLane.entry.distanceFromStart;
|
|
210
|
+
const laneExitDistance = pitLane.layout?.exitDistance ?? pitLane.exit.distanceFromStart;
|
|
211
|
+
const scratch = track?.queryIndex ? ensureQueryScratch(track.queryIndex) : null;
|
|
212
|
+
const pitRoutes = track?.queryIndex?.pit?.routes ?? {};
|
|
213
|
+
const projectionTarget = scratch ? (scratch.pitRoadProjection ??= { point: {} }) : null;
|
|
214
|
+
const projectionScratch = scratch ? (scratch.pitRoadProjectionScratch ??= {}) : null;
|
|
215
|
+
const useEntryEndpoint = inEntryConnector || nearEntryProgress;
|
|
216
|
+
const useExitEndpoint = inExitConnector || nearExitProgress;
|
|
217
|
+
const useIndexedConnectorCandidates = inEntryConnector || inExitConnector;
|
|
218
|
+
const roadCandidatesByRoute = (useIndexedConnectorCandidates || (!nearEntryProgress && !nearExitProgress))
|
|
219
|
+
? queryPitRoadSegmentCandidatesByRoute(track, position, null, scratch, {
|
|
220
|
+
countQuery: false,
|
|
221
|
+
recordPathStats: !useIndexedConnectorCandidates,
|
|
222
|
+
})
|
|
223
|
+
: null;
|
|
224
|
+
let bestState = null;
|
|
225
|
+
|
|
226
|
+
if (useEntryEndpoint) {
|
|
227
|
+
const entrySegments = connectorRouteSegments(
|
|
228
|
+
roadCandidatesByRoute,
|
|
229
|
+
'entry',
|
|
230
|
+
pitRoutes.entry?.startSegmentWindow ?? null,
|
|
231
|
+
inEntryConnector,
|
|
232
|
+
);
|
|
233
|
+
bestState = pickPitState(bestState, createPitRoadState(track, position, pitLane, {
|
|
234
|
+
points: pitLane.entry.roadCenterline,
|
|
235
|
+
surface: 'pit-entry',
|
|
236
|
+
part: 'entry',
|
|
237
|
+
roadWidth: pitLane.width,
|
|
238
|
+
startDistance: pitLane.entry.distanceFromStart,
|
|
239
|
+
endDistance: laneEntryDistance,
|
|
240
|
+
projectionTarget,
|
|
241
|
+
projectionScratch,
|
|
242
|
+
cumulativeDistances: pitRoutes.entry?.cumulativeDistances ?? null,
|
|
243
|
+
segmentCandidates: entrySegments.segmentCandidates,
|
|
244
|
+
connectorProjectionStats: true,
|
|
245
|
+
connectorEndpointWindowProjection: entrySegments.endpointWindow,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (useEntryEndpoint || useExitEndpoint) {
|
|
250
|
+
const mainSegments = connectorRouteSegments(
|
|
251
|
+
roadCandidatesByRoute,
|
|
252
|
+
'main',
|
|
253
|
+
connectorRouteSegmentWindow(pitRoutes.main, useEntryEndpoint, useExitEndpoint),
|
|
254
|
+
useIndexedConnectorCandidates,
|
|
255
|
+
);
|
|
256
|
+
bestState = pickPitState(bestState, createPitRoadState(track, position, pitLane, {
|
|
257
|
+
points: pitLane.mainLane.points,
|
|
258
|
+
surface: 'pit-lane',
|
|
259
|
+
part: 'fast-lane',
|
|
260
|
+
roadWidth: pitLane.width,
|
|
261
|
+
startDistance: laneEntryDistance,
|
|
262
|
+
endDistance: laneExitDistance,
|
|
263
|
+
projectionTarget,
|
|
264
|
+
projectionScratch,
|
|
265
|
+
cumulativeDistances: pitRoutes.main?.cumulativeDistances ?? null,
|
|
266
|
+
segmentCandidates: mainSegments.segmentCandidates,
|
|
267
|
+
connectorProjectionStats: true,
|
|
268
|
+
connectorEndpointWindowProjection: mainSegments.endpointWindow,
|
|
269
|
+
}));
|
|
270
|
+
const workingSegments = connectorRouteSegments(
|
|
271
|
+
roadCandidatesByRoute,
|
|
272
|
+
'working',
|
|
273
|
+
connectorRouteSegmentWindow(pitRoutes.working, useEntryEndpoint, useExitEndpoint),
|
|
274
|
+
useIndexedConnectorCandidates,
|
|
275
|
+
);
|
|
276
|
+
bestState = pickPitState(bestState, createPitRoadState(track, position, pitLane, {
|
|
277
|
+
points: pitLane.workingLane?.points,
|
|
278
|
+
surface: 'pit-lane',
|
|
279
|
+
part: 'working-lane',
|
|
280
|
+
roadWidth: pitLane.workingLane?.width ?? 0,
|
|
281
|
+
startDistance: laneEntryDistance,
|
|
282
|
+
endDistance: laneExitDistance,
|
|
283
|
+
projectionTarget,
|
|
284
|
+
projectionScratch,
|
|
285
|
+
cumulativeDistances: pitRoutes.working?.cumulativeDistances ?? null,
|
|
286
|
+
segmentCandidates: workingSegments.segmentCandidates,
|
|
287
|
+
connectorProjectionStats: true,
|
|
288
|
+
connectorEndpointWindowProjection: workingSegments.endpointWindow,
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (useExitEndpoint) {
|
|
293
|
+
const exitSegments = connectorRouteSegments(
|
|
294
|
+
roadCandidatesByRoute,
|
|
295
|
+
'exit',
|
|
296
|
+
pitRoutes.exit?.endSegmentWindow ?? null,
|
|
297
|
+
inExitConnector,
|
|
298
|
+
);
|
|
299
|
+
bestState = pickPitState(bestState, createPitRoadState(track, position, pitLane, {
|
|
300
|
+
points: pitLane.exit.roadCenterline,
|
|
301
|
+
surface: 'pit-exit',
|
|
302
|
+
part: 'exit',
|
|
303
|
+
roadWidth: pitLane.width,
|
|
304
|
+
startDistance: laneExitDistance,
|
|
305
|
+
endDistance: pitLane.exit.distanceFromStart,
|
|
306
|
+
projectionTarget,
|
|
307
|
+
projectionScratch,
|
|
308
|
+
cumulativeDistances: pitRoutes.exit?.cumulativeDistances ?? null,
|
|
309
|
+
segmentCandidates: exitSegments.segmentCandidates,
|
|
310
|
+
connectorProjectionStats: true,
|
|
311
|
+
connectorEndpointWindowProjection: exitSegments.endpointWindow,
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return bestState;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function connectorRouteSegmentWindow(route, useStart, useEnd) {
|
|
319
|
+
if (!route) return null;
|
|
320
|
+
if (useStart && useEnd) return route.endpointSegmentWindow ?? null;
|
|
321
|
+
if (useStart) return route.startSegmentWindow ?? null;
|
|
322
|
+
if (useEnd) return route.endSegmentWindow ?? null;
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function connectorRouteSegments(candidatesByRoute, routeId, endpointWindow, preferIndexedCandidates) {
|
|
327
|
+
if (preferIndexedCandidates && candidatesByRoute) {
|
|
328
|
+
return {
|
|
329
|
+
segmentCandidates: candidatesByRoute[routeId] ?? [],
|
|
330
|
+
endpointWindow: false,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (Array.isArray(endpointWindow)) {
|
|
334
|
+
return {
|
|
335
|
+
segmentCandidates: endpointWindow,
|
|
336
|
+
endpointWindow: endpointWindow.length > 0,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
segmentCandidates: candidatesByRoute?.[routeId] ?? null,
|
|
341
|
+
endpointWindow: false,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function pickPitState(currentBest, candidate) {
|
|
346
|
+
if (!candidate) return currentBest;
|
|
347
|
+
if (!currentBest) return candidate;
|
|
348
|
+
return candidate.crossTrackError < currentBest.crossTrackError ? candidate : currentBest;
|
|
150
349
|
}
|
|
151
350
|
|
|
152
351
|
function createLegacyPitBoxCandidates(pitLane) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { clamp, normalizeAngle, wrapDistance } from '../simMath.js';
|
|
2
2
|
import { PIT_LANE_WIDTH } from './trackConstants.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { pointInsideBounds } from './trackMath.js';
|
|
4
|
+
import { nearestPitLaneState, resolveDirectConnectorPitLaneState } from './pitLaneState.js';
|
|
5
|
+
import { queryNearestTrackProjection, queryNearestTrackProjectionInto } from './trackQueryIndex.js';
|
|
6
|
+
import { recordStat } from './trackQueryStats.js';
|
|
5
7
|
|
|
6
8
|
export function pointAt(track, distanceAlong) {
|
|
7
9
|
const wrapped = wrapDistance(distanceAlong, track.length);
|
|
@@ -62,6 +64,10 @@ export function nearestSampleGlobal(track, position) {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export function createTrackState(track, position, best) {
|
|
67
|
+
return writeTrackState({}, track, position, best);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function writeTrackState(target, track, position, best) {
|
|
65
71
|
const dx = position.x - best.x;
|
|
66
72
|
const dy = position.y - best.y;
|
|
67
73
|
const signedOffset = dx * best.normalX + dy * best.normalY;
|
|
@@ -80,24 +86,47 @@ export function createTrackState(track, position, best) {
|
|
|
80
86
|
? 'grass'
|
|
81
87
|
: 'barrier';
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
target.segmentId = best.segmentId;
|
|
90
|
+
target.x = best.x;
|
|
91
|
+
target.y = best.y;
|
|
92
|
+
target.distance = best.distance;
|
|
93
|
+
target.heading = best.heading;
|
|
94
|
+
target.normalX = best.normalX;
|
|
95
|
+
target.normalY = best.normalY;
|
|
96
|
+
target.curvature = best.curvature;
|
|
97
|
+
target.signedOffset = signedOffset;
|
|
98
|
+
target.crossTrackError = crossTrackError;
|
|
99
|
+
target.surface = surface;
|
|
100
|
+
target.onTrack = surface === 'track' || surface === 'kerb';
|
|
101
|
+
target.distanceSquared = best.distanceSquared;
|
|
102
|
+
target.inPitLane = undefined;
|
|
103
|
+
target.pitLanePart = undefined;
|
|
104
|
+
target.pitBoxId = undefined;
|
|
105
|
+
target.mainTrackSignedOffset = undefined;
|
|
106
|
+
target.mainTrackCrossTrackError = undefined;
|
|
107
|
+
return target;
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
export function nearestTrackState(track, position, progressHint = null, options = {}) {
|
|
93
111
|
const allowPitOverride = options.allowPitOverride !== false;
|
|
94
|
-
const
|
|
112
|
+
const scratch = track?.queryIndex?.queryScratch ?? null;
|
|
113
|
+
const best = queryNearestTrackProjectionInto(
|
|
114
|
+
track,
|
|
115
|
+
position,
|
|
116
|
+
progressHint,
|
|
117
|
+
scratch ? (scratch.nearestProjection ??= {}) : null,
|
|
118
|
+
);
|
|
95
119
|
if (!best) {
|
|
96
120
|
throw new Error('nearestTrackState requires a finite position and an indexed track model. Build tracks with buildTrackModel() or createRaceSimulation().');
|
|
97
121
|
}
|
|
98
122
|
const trackState = createTrackState(track, position, best);
|
|
99
123
|
if (!allowPitOverride) return trackState;
|
|
100
|
-
const pitState =
|
|
124
|
+
const pitState = queryPitOverrideState(track, position, progressHint, trackState);
|
|
125
|
+
if (!pitState) return trackState;
|
|
126
|
+
return resolveTrackStatePitOverride(track, trackState, pitState);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function resolveTrackStatePitOverride(track, trackState, pitState) {
|
|
101
130
|
if (!pitState) return trackState;
|
|
102
131
|
|
|
103
132
|
const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
|
|
@@ -122,6 +151,33 @@ export function nearestTrackState(track, position, progressHint = null, options
|
|
|
122
151
|
};
|
|
123
152
|
}
|
|
124
153
|
|
|
154
|
+
function queryPitOverrideState(track, position, progressHint, trackState) {
|
|
155
|
+
const pitLane = track?.pitLane;
|
|
156
|
+
if (!pitLane?.enabled) return null;
|
|
157
|
+
if (!pointInsideBounds(position, pitLane.bounds)) return null;
|
|
158
|
+
if (pitLane.boxBounds && pointInsideBounds(position, pitLane.boxBounds)) {
|
|
159
|
+
return nearestPitLaneState(track, position, progressHint);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
|
|
163
|
+
if (trackState.crossTrackError <= mainRoadEdge) {
|
|
164
|
+
recordStat(track?.queryIndex, 'pitPaths', 'main-road-skip');
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if ((trackState.signedOffset ?? 0) * pitLane.side > 0) {
|
|
168
|
+
const directConnectorState = resolveDirectConnectorPitLaneState(track, position, progressHint);
|
|
169
|
+
if (directConnectorState !== undefined) {
|
|
170
|
+
if (directConnectorState) {
|
|
171
|
+
recordStat(track?.queryIndex, 'pitPaths', 'connector-direct-state');
|
|
172
|
+
return directConnectorState;
|
|
173
|
+
}
|
|
174
|
+
recordStat(track?.queryIndex, 'pitPaths', 'connector-direct-skip');
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return nearestPitLaneState(track, position, progressHint);
|
|
179
|
+
}
|
|
180
|
+
|
|
125
181
|
export function offsetTrackPoint(point, offset) {
|
|
126
182
|
return {
|
|
127
183
|
x: point.x + point.normalX * offset,
|