@inventure71/paddockjs 2.0.1 → 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 +176 -1
- package/INSTALL_AND_UPDATE.md +24 -4
- package/README.md +84 -512
- package/docs/architecture.md +38 -24
- package/docs/component_inventory.md +72 -0
- package/docs/composable-layouts.md +77 -0
- package/docs/concepts.md +2 -2
- package/docs/custom_model_controller.md +6 -6
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +104 -27
- package/docs/getting-started.md +75 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +18 -4
- package/docs/learnings.md +6 -6
- package/docs/rules.md +12 -8
- package/docs/sense_contract.md +1 -1
- package/docs/system_specs.md +71 -21
- package/docs/theming.md +64 -0
- package/docs/training.md +26 -19
- 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 +150 -24
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +213 -29
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +262 -49
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +58 -38
- package/src/config/themeOptions.js +830 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/controllerLoop.js +17 -3
- package/src/environment/episode.js +11 -1
- package/src/environment/index.d.ts +1 -2
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/options.js +1 -1
- package/src/environment/runtime.js +44 -11
- package/src/environment/scenarios.js +1 -1
- 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 +167 -118
- package/src/environment/sensors/trackRays.js +157 -48
- package/src/index.d.ts +153 -9
- package/src/index.js +52 -3
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitKinematics.js +2 -2
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +79 -46
- 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 +4 -6
- package/src/simulation/race/raceStep.js +26 -14
- package/src/simulation/race/safetyCar.js +1 -1
- package/src/simulation/race/stalledDnf.js +1 -1
- 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 +138 -40
- 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 +30 -3
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +66 -33
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1468 -189
- 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 -2
- package/src/simulation/vehicle/contactResolution.js +99 -62
- 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/vehicleKinematics.js +2 -2
- package/src/simulation/vehicle/vehiclePhysics.js +45 -30
- package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
- package/src/simulation/vehicle/vehicleState.js +7 -1
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1568 -519
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- package/src/ui/timingTowerTemplate.js +29 -12
|
@@ -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
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { PIT_LANE_WIDTH } from './trackConstants.js';
|
|
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,27 +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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
})
|
|
100
|
-
|
|
112
|
+
const scratch = track?.queryIndex?.queryScratch ?? null;
|
|
113
|
+
const best = queryNearestTrackProjectionInto(
|
|
114
|
+
track,
|
|
115
|
+
position,
|
|
116
|
+
progressHint,
|
|
117
|
+
scratch ? (scratch.nearestProjection ??= {}) : null,
|
|
118
|
+
);
|
|
119
|
+
if (!best) {
|
|
120
|
+
throw new Error('nearestTrackState requires a finite position and an indexed track model. Build tracks with buildTrackModel() or createRaceSimulation().');
|
|
121
|
+
}
|
|
101
122
|
const trackState = createTrackState(track, position, best);
|
|
102
123
|
if (!allowPitOverride) return trackState;
|
|
103
|
-
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) {
|
|
104
130
|
if (!pitState) return trackState;
|
|
105
131
|
|
|
106
132
|
const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
|
|
@@ -125,24 +151,31 @@ export function nearestTrackState(track, position, progressHint = null, options
|
|
|
125
151
|
};
|
|
126
152
|
}
|
|
127
153
|
|
|
128
|
-
function
|
|
129
|
-
|
|
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
|
+
}
|
|
130
161
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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;
|
|
142
176
|
}
|
|
143
177
|
}
|
|
144
|
-
|
|
145
|
-
return nearest?.best ?? nearestSampleGlobal(track, position).best;
|
|
178
|
+
return nearestPitLaneState(track, position, progressHint);
|
|
146
179
|
}
|
|
147
180
|
|
|
148
181
|
export function offsetTrackPoint(point, offset) {
|
|
@@ -127,6 +127,16 @@ export function expandBounds(bounds, point) {
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
export function expandBoundsByPadding(bounds, padding = 0) {
|
|
131
|
+
const resolvedPadding = Number.isFinite(padding) ? Math.max(0, padding) : 0;
|
|
132
|
+
return {
|
|
133
|
+
minX: bounds.minX - resolvedPadding,
|
|
134
|
+
maxX: bounds.maxX + resolvedPadding,
|
|
135
|
+
minY: bounds.minY - resolvedPadding,
|
|
136
|
+
maxY: bounds.maxY + resolvedPadding,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
export function createPointBounds(points, padding = 0) {
|
|
131
141
|
const bounds = points.filter(Boolean).reduce(
|
|
132
142
|
(current, point) => expandBounds(current, point),
|
|
@@ -181,43 +191,100 @@ export function projectPointToSegment(position, start, end) {
|
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
export function nearestPointOnPolyline(points, position, segmentIndices = null) {
|
|
194
|
+
return nearestPointOnPolylineInto({}, points, position, { segmentIndices });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function nearestPointOnPolylineInto(target, points, position, {
|
|
198
|
+
segmentIndices = null,
|
|
199
|
+
scratch = null,
|
|
200
|
+
cumulativeDistances: precomputedCumulativeDistances = null,
|
|
201
|
+
} = {}) {
|
|
184
202
|
if (!Array.isArray(points) || points.length < 2) return null;
|
|
185
|
-
const
|
|
186
|
-
|
|
203
|
+
const output = target ?? {};
|
|
204
|
+
const outputPoint = output.point ?? {};
|
|
205
|
+
output.point = outputPoint;
|
|
206
|
+
const restrictedSegments = Array.isArray(segmentIndices) && segmentIndices.length > 0;
|
|
207
|
+
const cumulativeDistances = restrictedSegments
|
|
208
|
+
? (Array.isArray(precomputedCumulativeDistances) && precomputedCumulativeDistances.length >= points.length
|
|
209
|
+
? precomputedCumulativeDistances
|
|
210
|
+
: writePolylineCumulativeDistances(scratch?.cumulativeDistances ?? new Array(points.length), points))
|
|
187
211
|
: null;
|
|
188
|
-
|
|
189
|
-
|
|
212
|
+
if (scratch && restrictedSegments && cumulativeDistances !== precomputedCumulativeDistances) {
|
|
213
|
+
scratch.cumulativeDistances = cumulativeDistances;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let bestSegmentIndex = -1;
|
|
217
|
+
let bestAmount = 0;
|
|
218
|
+
let bestLength = 0;
|
|
219
|
+
let bestHeading = 0;
|
|
220
|
+
let bestNormalX = 0;
|
|
221
|
+
let bestNormalY = 1;
|
|
222
|
+
let bestSignedOffset = 0;
|
|
223
|
+
let bestCrossTrackError = Infinity;
|
|
224
|
+
let bestDistanceAlong = 0;
|
|
225
|
+
let bestX = 0;
|
|
226
|
+
let bestY = 0;
|
|
190
227
|
let distanceBefore = 0;
|
|
191
228
|
let totalLength = 0;
|
|
192
229
|
|
|
193
|
-
const segmentCount = restrictedSegments
|
|
230
|
+
const segmentCount = restrictedSegments ? segmentIndices.length : (points.length - 1);
|
|
194
231
|
for (let segmentOffset = 0; segmentOffset < segmentCount; segmentOffset += 1) {
|
|
195
|
-
const index = restrictedSegments ?
|
|
232
|
+
const index = restrictedSegments ? segmentIndices[segmentOffset] : segmentOffset;
|
|
233
|
+
if (!Number.isInteger(index) || index < 0 || index >= points.length - 1) continue;
|
|
196
234
|
const current = points[index];
|
|
197
235
|
const next = points[index + 1];
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
236
|
+
const dx = next.x - current.x;
|
|
237
|
+
const dy = next.y - current.y;
|
|
238
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
239
|
+
const amount = lengthSquared > 0
|
|
240
|
+
? clamp(((position.x - current.x) * dx + (position.y - current.y) * dy) / lengthSquared, 0, 1)
|
|
241
|
+
: 0;
|
|
242
|
+
const projectedX = current.x + dx * amount;
|
|
243
|
+
const projectedY = current.y + dy * amount;
|
|
244
|
+
const length = Math.sqrt(lengthSquared);
|
|
245
|
+
const heading = Math.atan2(dy, dx);
|
|
246
|
+
const normalX = length > 0 ? -dy / length : 0;
|
|
247
|
+
const normalY = length > 0 ? dx / length : 1;
|
|
248
|
+
const signedOffset = (position.x - projectedX) * normalX + (position.y - projectedY) * normalY;
|
|
249
|
+
const crossTrackError = Math.abs(signedOffset);
|
|
250
|
+
if (crossTrackError < bestCrossTrackError) {
|
|
251
|
+
bestSegmentIndex = index;
|
|
252
|
+
bestAmount = amount;
|
|
253
|
+
bestLength = length;
|
|
254
|
+
bestHeading = heading;
|
|
255
|
+
bestNormalX = normalX;
|
|
256
|
+
bestNormalY = normalY;
|
|
257
|
+
bestSignedOffset = signedOffset;
|
|
258
|
+
bestCrossTrackError = crossTrackError;
|
|
259
|
+
bestDistanceAlong = (restrictedSegments ? cumulativeDistances[index] : distanceBefore) + length * amount;
|
|
260
|
+
bestX = projectedX;
|
|
261
|
+
bestY = projectedY;
|
|
205
262
|
}
|
|
206
|
-
if (!restrictedSegments) distanceBefore +=
|
|
207
|
-
totalLength +=
|
|
263
|
+
if (!restrictedSegments) distanceBefore += length;
|
|
264
|
+
totalLength += length;
|
|
208
265
|
}
|
|
209
266
|
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
267
|
+
if (bestSegmentIndex < 0) return null;
|
|
268
|
+
outputPoint.x = bestX;
|
|
269
|
+
outputPoint.y = bestY;
|
|
270
|
+
output.amount = bestAmount;
|
|
271
|
+
output.length = bestLength;
|
|
272
|
+
output.heading = bestHeading;
|
|
273
|
+
output.normalX = bestNormalX;
|
|
274
|
+
output.normalY = bestNormalY;
|
|
275
|
+
output.signedOffset = bestSignedOffset;
|
|
276
|
+
output.crossTrackError = bestCrossTrackError;
|
|
277
|
+
output.segmentIndex = bestSegmentIndex;
|
|
278
|
+
output.distanceAlong = bestDistanceAlong;
|
|
279
|
+
output.totalLength = restrictedSegments
|
|
280
|
+
? cumulativeDistances[cumulativeDistances.length - 1]
|
|
281
|
+
: totalLength;
|
|
282
|
+
return output;
|
|
217
283
|
}
|
|
218
284
|
|
|
219
|
-
function
|
|
220
|
-
|
|
285
|
+
function writePolylineCumulativeDistances(distances, points) {
|
|
286
|
+
distances.length = points.length;
|
|
287
|
+
distances[0] = 0;
|
|
221
288
|
for (let index = 1; index < points.length; index += 1) {
|
|
222
289
|
distances[index] = distances[index - 1] + distance(points[index - 1], points[index]);
|
|
223
290
|
}
|
|
@@ -11,6 +11,7 @@ import { deriveDrsZones, normalizeDrsZone } from './drsZones.js';
|
|
|
11
11
|
import { createPitLaneModel } from './pitLaneLayout.js';
|
|
12
12
|
import { attachTrackQueryIndex, createTrackQueryIndex } from './trackQueryIndex.js';
|
|
13
13
|
import { resolveProceduralTrackOptions } from './trackGenerationOptions.js';
|
|
14
|
+
import { attachTrackSnapshotJsonSerializer } from './trackSnapshotJson.js';
|
|
14
15
|
|
|
15
16
|
export { WORLD, TRACK } from './trackConstants.js';
|
|
16
17
|
export { isInDrsZone } from './drsZones.js';
|
|
@@ -56,6 +57,7 @@ export function buildTrackModel(track = TRACK) {
|
|
|
56
57
|
? null
|
|
57
58
|
: createPitLaneModel(model);
|
|
58
59
|
attachTrackQueryIndex(model, createTrackQueryIndex(model));
|
|
60
|
+
attachTrackSnapshotJsonSerializer(model);
|
|
59
61
|
if (canReuseCachedModel) {
|
|
60
62
|
TRACK_MODEL_CACHE.set(track, freezeTrackModel(model));
|
|
61
63
|
}
|