@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
|
@@ -12,11 +12,21 @@ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
|
|
|
12
12
|
import { pointOnRay } from './rayGeometry.js';
|
|
13
13
|
import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
|
|
14
14
|
|
|
15
|
-
const LEGAL_SURFACES = new Set(['track', 'kerb', 'pit-entry', 'pit-lane', 'pit-exit', 'pit-box']);
|
|
16
|
-
const SURFACE_CHANNELS = new Set(['kerb', 'illegalSurface']);
|
|
17
|
-
|
|
18
15
|
export function requestedSurfaceChannels(channels = []) {
|
|
19
|
-
|
|
16
|
+
const requested = [];
|
|
17
|
+
let hasKerb = false;
|
|
18
|
+
let hasIllegalSurface = false;
|
|
19
|
+
for (let index = 0; index < channels.length; index += 1) {
|
|
20
|
+
const channel = channels[index];
|
|
21
|
+
if (channel === 'kerb' && !hasKerb) {
|
|
22
|
+
requested.push(channel);
|
|
23
|
+
hasKerb = true;
|
|
24
|
+
} else if (channel === 'illegalSurface' && !hasIllegalSurface) {
|
|
25
|
+
requested.push(channel);
|
|
26
|
+
hasIllegalSurface = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return requested;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export function createSurfaceMiss(lengthMeters) {
|
|
@@ -38,15 +48,15 @@ export function estimateSurfaceHits(
|
|
|
38
48
|
{ precision = 'driver', sharedRayQuery = null } = {},
|
|
39
49
|
) {
|
|
40
50
|
const requested = requestedSurfaceChannels(channels);
|
|
41
|
-
const
|
|
51
|
+
const hits = prepareSurfaceHitContainer(sharedRayQuery, ray.lengthMeters);
|
|
42
52
|
if (!requested.length || !Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
|
|
43
|
-
return
|
|
53
|
+
return hits;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
const originState = context?.precision === precision
|
|
47
57
|
? context.originState
|
|
48
58
|
: nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
|
|
49
|
-
|
|
59
|
+
writeSurfaceHitsAtOrigin(hits, requested, originState);
|
|
50
60
|
const indexedHits = estimateIndexedSurfaceHits({
|
|
51
61
|
car,
|
|
52
62
|
track: snapshot.track,
|
|
@@ -57,8 +67,9 @@ export function estimateSurfaceHits(
|
|
|
57
67
|
originState,
|
|
58
68
|
precision,
|
|
59
69
|
sharedRayQuery,
|
|
70
|
+
hits,
|
|
60
71
|
});
|
|
61
|
-
if (indexedHits) return
|
|
72
|
+
if (indexedHits) return indexedHits;
|
|
62
73
|
const analyticHits = estimateAnalyticSurfaceHits({
|
|
63
74
|
car,
|
|
64
75
|
track: snapshot.track,
|
|
@@ -66,15 +77,13 @@ export function estimateSurfaceHits(
|
|
|
66
77
|
vector,
|
|
67
78
|
requested,
|
|
68
79
|
originState,
|
|
80
|
+
hits,
|
|
69
81
|
});
|
|
70
|
-
if (analyticHits) return
|
|
82
|
+
if (analyticHits) return analyticHits;
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
pending.delete(channel);
|
|
76
|
-
});
|
|
77
|
-
if (pending.size === 0) return misses;
|
|
84
|
+
let pendingKerb = requestedIncludes(requested, 'kerb') && !matchesSurfaceChannel('kerb', originState);
|
|
85
|
+
let pendingIllegalSurface = requestedIncludes(requested, 'illegalSurface') && !matchesSurfaceChannel('illegalSurface', originState);
|
|
86
|
+
if (!pendingKerb && !pendingIllegalSurface) return hits;
|
|
78
87
|
const maxDistance = metersToSimUnits(ray.lengthMeters);
|
|
79
88
|
const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
|
|
80
89
|
let previousDistance = 0;
|
|
@@ -82,60 +91,78 @@ export function estimateSurfaceHits(
|
|
|
82
91
|
|
|
83
92
|
for (let distance = 0; distance <= maxDistance; distance += step) {
|
|
84
93
|
const state = nearestRayTrackState(snapshot.track, car, pointOnRay(origin, vector, distance), car.progress, precision);
|
|
85
|
-
|
|
86
|
-
if (!matchesSurfaceChannel(channel, state)) continue;
|
|
94
|
+
if (pendingKerb && matchesSurfaceChannel('kerb', state)) {
|
|
87
95
|
const hitDistance = previousState
|
|
88
|
-
? refineSurfaceTransition(snapshot.track, car, origin, vector, car.progress, previousDistance, distance,
|
|
96
|
+
? refineSurfaceTransition(snapshot.track, car, origin, vector, car.progress, previousDistance, distance, 'kerb', refineStepsForPrecision(precision))
|
|
89
97
|
: distance;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
distanceMeters: simUnitsToMeters(hitDistance),
|
|
93
|
-
surface: state.surface ?? null,
|
|
94
|
-
};
|
|
95
|
-
pending.delete(channel);
|
|
98
|
+
writeSurfaceHit(hits.kerb, simUnitsToMeters(hitDistance), state.surface ?? null);
|
|
99
|
+
pendingKerb = false;
|
|
96
100
|
}
|
|
97
|
-
if (
|
|
101
|
+
if (pendingIllegalSurface && matchesSurfaceChannel('illegalSurface', state)) {
|
|
102
|
+
const hitDistance = previousState
|
|
103
|
+
? refineSurfaceTransition(snapshot.track, car, origin, vector, car.progress, previousDistance, distance, 'illegalSurface', refineStepsForPrecision(precision))
|
|
104
|
+
: distance;
|
|
105
|
+
writeSurfaceHit(hits.illegalSurface, simUnitsToMeters(hitDistance), state.surface ?? null);
|
|
106
|
+
pendingIllegalSurface = false;
|
|
107
|
+
}
|
|
108
|
+
if (!pendingKerb && !pendingIllegalSurface) break;
|
|
98
109
|
previousDistance = distance;
|
|
99
110
|
previousState = state;
|
|
100
111
|
}
|
|
101
112
|
|
|
102
|
-
return
|
|
113
|
+
return hits;
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
function
|
|
106
|
-
const hits =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
surface: originState.surface ?? null,
|
|
113
|
-
};
|
|
114
|
-
});
|
|
116
|
+
function prepareSurfaceHitContainer(cache, lengthMeters) {
|
|
117
|
+
const hits = cache
|
|
118
|
+
? (cache.surfaceHits ?? {})
|
|
119
|
+
: {};
|
|
120
|
+
if (cache) cache.surfaceHits = hits;
|
|
121
|
+
hits.kerb = writeSurfaceMiss(hits.kerb ?? {}, lengthMeters);
|
|
122
|
+
hits.illegalSurface = writeSurfaceMiss(hits.illegalSurface ?? {}, lengthMeters);
|
|
115
123
|
return hits;
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
function
|
|
126
|
+
function writeSurfaceMiss(target, lengthMeters) {
|
|
127
|
+
target.hit = false;
|
|
128
|
+
target.distanceMeters = lengthMeters;
|
|
129
|
+
target.surface = null;
|
|
130
|
+
return target;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function writeSurfaceHit(target, distanceMeters, surface) {
|
|
134
|
+
target.hit = true;
|
|
135
|
+
target.distanceMeters = distanceMeters;
|
|
136
|
+
target.surface = surface ?? null;
|
|
137
|
+
return target;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function writeSurfaceHitsAtOrigin(hits, requested, originState) {
|
|
141
|
+
for (let index = 0; index < requested.length; index += 1) {
|
|
142
|
+
const channel = requested[index];
|
|
143
|
+
if (!matchesSurfaceChannel(channel, originState)) continue;
|
|
144
|
+
writeSurfaceHit(hits[channel], 0, originState.surface ?? null);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, originState, hits }) {
|
|
119
149
|
if (!originState || car.inPitLane || originState.inPitLane) return null;
|
|
120
150
|
if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
|
|
121
151
|
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
|
|
122
152
|
if (Math.abs(originState.curvature ?? 0) > ANALYTIC_TRACK_RAY_MAX_CURVATURE) return null;
|
|
123
153
|
|
|
124
154
|
const lateral = vector.x * originState.normalX + vector.y * originState.normalY;
|
|
125
|
-
if (Math.abs(lateral) < 0.08) return
|
|
126
|
-
channel,
|
|
127
|
-
createSurfaceMiss(ray.lengthMeters),
|
|
128
|
-
]));
|
|
155
|
+
if (Math.abs(lateral) < 0.08) return hits;
|
|
129
156
|
|
|
130
157
|
const trackHalfWidth = track.width / 2;
|
|
131
158
|
const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
|
|
132
159
|
const maxDistance = metersToSimUnits(ray.lengthMeters);
|
|
133
160
|
const offset = originState.signedOffset ?? car.signedOffset ?? 0;
|
|
134
|
-
const hits = {};
|
|
135
161
|
|
|
136
|
-
requested.
|
|
162
|
+
for (let index = 0; index < requested.length; index += 1) {
|
|
163
|
+
const channel = requested[index];
|
|
137
164
|
if (channel === 'kerb') {
|
|
138
|
-
hits
|
|
165
|
+
hitAbsOffsetBand(hits.kerb, {
|
|
139
166
|
offset,
|
|
140
167
|
lateral,
|
|
141
168
|
minAbsOffset: trackHalfWidth,
|
|
@@ -145,7 +172,7 @@ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, origi
|
|
|
145
172
|
surface: 'kerb',
|
|
146
173
|
});
|
|
147
174
|
} else if (channel === 'illegalSurface') {
|
|
148
|
-
hits
|
|
175
|
+
hitAbsOffsetBand(hits.illegalSurface, {
|
|
149
176
|
offset,
|
|
150
177
|
lateral,
|
|
151
178
|
minAbsOffset: kerbOuter,
|
|
@@ -155,12 +182,12 @@ function estimateAnalyticSurfaceHits({ car, track, ray, vector, requested, origi
|
|
|
155
182
|
surface: surfaceBeyondKerb(track, offset, lateral, maxDistance),
|
|
156
183
|
});
|
|
157
184
|
}
|
|
158
|
-
}
|
|
185
|
+
}
|
|
159
186
|
|
|
160
187
|
return hits;
|
|
161
188
|
}
|
|
162
189
|
|
|
163
|
-
function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery }) {
|
|
190
|
+
function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested, originState, precision, sharedRayQuery, hits }) {
|
|
164
191
|
if (!originState || car.inPitLane || originState.inPitLane) return null;
|
|
165
192
|
if (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState)) return null;
|
|
166
193
|
|
|
@@ -177,24 +204,24 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
|
|
|
177
204
|
sharedRayQuery,
|
|
178
205
|
);
|
|
179
206
|
if (!boundaries.available) return null;
|
|
180
|
-
const hits = {};
|
|
181
207
|
|
|
182
|
-
for (
|
|
208
|
+
for (let index = 0; index < requested.length; index += 1) {
|
|
209
|
+
const channel = requested[index];
|
|
183
210
|
if (matchesSurfaceChannel(channel, originState)) {
|
|
184
|
-
hits[channel]
|
|
211
|
+
writeSurfaceHit(hits[channel], 0, originState.surface ?? null);
|
|
185
212
|
continue;
|
|
186
213
|
}
|
|
187
214
|
|
|
188
215
|
const boundaryDistance = channel === 'kerb'
|
|
189
|
-
?
|
|
216
|
+
? minFinite2(boundaries.trackEdgeDistance, boundaries.kerbOuterDistance)
|
|
190
217
|
: boundaries.kerbOuterDistance;
|
|
191
218
|
if (boundaryDistance == null) {
|
|
192
219
|
if (usesMainTrackOnlyRays(car) && channel === 'illegalSurface') {
|
|
193
|
-
hits[channel]
|
|
220
|
+
writeSurfaceMiss(hits[channel], ray.lengthMeters);
|
|
194
221
|
continue;
|
|
195
222
|
}
|
|
196
223
|
if (canSampleRecoverySurface(track, originState, vector)) return null;
|
|
197
|
-
hits[channel]
|
|
224
|
+
writeSurfaceMiss(hits[channel], ray.lengthMeters);
|
|
198
225
|
continue;
|
|
199
226
|
}
|
|
200
227
|
const hitDistance = usesMainTrackOnlyRays(car) || refineStepsForPrecision(precision) <= 0 || precision === 'debug'
|
|
@@ -211,13 +238,13 @@ function estimateIndexedSurfaceHits({ car, track, ray, origin, vector, requested
|
|
|
211
238
|
precision,
|
|
212
239
|
});
|
|
213
240
|
if (hitDistance == null) return null;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
241
|
+
writeSurfaceHit(
|
|
242
|
+
hits[channel],
|
|
243
|
+
simUnitsToMeters(hitDistance),
|
|
244
|
+
channel === 'kerb'
|
|
218
245
|
? 'kerb'
|
|
219
246
|
: surfaceBeyondKerb(track, originState.signedOffset ?? 0, lateral, metersToSimUnits(ray.lengthMeters)),
|
|
220
|
-
|
|
247
|
+
);
|
|
221
248
|
}
|
|
222
249
|
|
|
223
250
|
return hits;
|
|
@@ -249,35 +276,28 @@ function usesMainTrackOnlyRays(car) {
|
|
|
249
276
|
return car?.interaction?.profile === 'batch-training';
|
|
250
277
|
}
|
|
251
278
|
|
|
252
|
-
function hitAbsOffsetBand({ offset, lateral, minAbsOffset, maxAbsOffset, maxDistance, lengthMeters, surface }) {
|
|
279
|
+
function hitAbsOffsetBand(target, { offset, lateral, minAbsOffset, maxAbsOffset, maxDistance, lengthMeters, surface }) {
|
|
253
280
|
const currentAbs = Math.abs(offset);
|
|
254
281
|
if (currentAbs >= minAbsOffset && currentAbs <= maxAbsOffset) {
|
|
255
|
-
return
|
|
282
|
+
return writeSurfaceHit(target, 0, surface);
|
|
256
283
|
}
|
|
257
284
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance);
|
|
262
|
-
|
|
285
|
+
let distance = Infinity;
|
|
286
|
+
distance = nearestValidBandDistance(distance, distanceToOffset(minAbsOffset, offset, lateral), offset, lateral, minAbsOffset, maxAbsOffset, maxDistance);
|
|
287
|
+
distance = nearestValidBandDistance(distance, distanceToOffset(-minAbsOffset, offset, lateral), offset, lateral, minAbsOffset, maxAbsOffset, maxDistance);
|
|
263
288
|
if (Number.isFinite(maxAbsOffset)) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
distanceToOffset(maxAbsOffset, offset, lateral),
|
|
267
|
-
distanceToOffset(-maxAbsOffset, offset, lateral),
|
|
268
|
-
].filter((distance) => Number.isFinite(distance) && distance >= 0 && distance <= maxDistance),
|
|
269
|
-
);
|
|
289
|
+
distance = nearestValidBandDistance(distance, distanceToOffset(maxAbsOffset, offset, lateral), offset, lateral, minAbsOffset, maxAbsOffset, maxDistance);
|
|
290
|
+
distance = nearestValidBandDistance(distance, distanceToOffset(-maxAbsOffset, offset, lateral), offset, lateral, minAbsOffset, maxAbsOffset, maxDistance);
|
|
270
291
|
}
|
|
271
292
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return nextAbs >= minAbsOffset - 1e-6 && nextAbs <= maxAbsOffset + 1e-6;
|
|
276
|
-
})
|
|
277
|
-
.sort((a, b) => a - b)[0];
|
|
293
|
+
if (!Number.isFinite(distance)) return writeSurfaceMiss(target, lengthMeters);
|
|
294
|
+
return writeSurfaceHit(target, simUnitsToMeters(distance), surface);
|
|
295
|
+
}
|
|
278
296
|
|
|
279
|
-
|
|
280
|
-
|
|
297
|
+
function nearestValidBandDistance(current, candidate, offset, lateral, minAbsOffset, maxAbsOffset, maxDistance) {
|
|
298
|
+
if (!Number.isFinite(candidate) || candidate < 0 || candidate > maxDistance || candidate >= current) return current;
|
|
299
|
+
const nextAbs = Math.abs(offset + lateral * candidate);
|
|
300
|
+
return nextAbs >= minAbsOffset - 1e-6 && nextAbs <= maxAbsOffset + 1e-6 ? candidate : current;
|
|
281
301
|
}
|
|
282
302
|
|
|
283
303
|
function distanceToOffset(targetOffset, offset, lateral) {
|
|
@@ -286,12 +306,16 @@ function distanceToOffset(targetOffset, offset, lateral) {
|
|
|
286
306
|
|
|
287
307
|
function surfaceBeyondKerb(track, offset, lateral, maxDistance) {
|
|
288
308
|
const gravelOuter = track.width / 2 + (track.kerbWidth ?? 0) + (track.gravelWidth ?? 0);
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
309
|
+
const kerbOuter = track.width / 2 + (track.kerbWidth ?? 0);
|
|
310
|
+
const positiveDistance = distanceToOffset(kerbOuter, offset, lateral);
|
|
311
|
+
const negativeDistance = distanceToOffset(-kerbOuter, offset, lateral);
|
|
312
|
+
let hitDistance = Infinity;
|
|
313
|
+
if (Number.isFinite(positiveDistance) && positiveDistance >= 0 && positiveDistance <= maxDistance) {
|
|
314
|
+
hitDistance = positiveDistance;
|
|
315
|
+
}
|
|
316
|
+
if (Number.isFinite(negativeDistance) && negativeDistance >= 0 && negativeDistance <= maxDistance && negativeDistance < hitDistance) {
|
|
317
|
+
hitDistance = negativeDistance;
|
|
318
|
+
}
|
|
295
319
|
const hitAbs = Math.abs(offset + lateral * hitDistance);
|
|
296
320
|
return hitAbs <= gravelOuter ? 'gravel' : 'grass';
|
|
297
321
|
}
|
|
@@ -342,10 +366,26 @@ function refineStepsForPrecision(precision) {
|
|
|
342
366
|
|
|
343
367
|
function matchesSurfaceChannel(channel, state) {
|
|
344
368
|
if (channel === 'kerb') return state.surface === 'kerb';
|
|
345
|
-
if (channel === 'illegalSurface') return !
|
|
369
|
+
if (channel === 'illegalSurface') return !isLegalSurface(state.surface) && state.surface !== 'barrier';
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function requestedIncludes(requested, channel) {
|
|
374
|
+
for (let index = 0; index < requested.length; index += 1) {
|
|
375
|
+
if (requested[index] === channel) return true;
|
|
376
|
+
}
|
|
346
377
|
return false;
|
|
347
378
|
}
|
|
348
379
|
|
|
380
|
+
function isLegalSurface(surface) {
|
|
381
|
+
return surface === 'track' ||
|
|
382
|
+
surface === 'kerb' ||
|
|
383
|
+
surface === 'pit-entry' ||
|
|
384
|
+
surface === 'pit-lane' ||
|
|
385
|
+
surface === 'pit-exit' ||
|
|
386
|
+
surface === 'pit-box';
|
|
387
|
+
}
|
|
388
|
+
|
|
349
389
|
function canSampleRecoverySurface(track, originState, vector) {
|
|
350
390
|
if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
|
|
351
391
|
const kerbOuter = track.width / 2 + (track.kerbWidth ?? 0);
|
|
@@ -362,7 +402,8 @@ function nearestRayTrackState(track, car, point, progressHint, precision = 'debu
|
|
|
362
402
|
});
|
|
363
403
|
}
|
|
364
404
|
|
|
365
|
-
function
|
|
366
|
-
|
|
367
|
-
|
|
405
|
+
function minFinite2(first, second) {
|
|
406
|
+
if (!Number.isFinite(first)) return Number.isFinite(second) ? second : null;
|
|
407
|
+
if (!Number.isFinite(second)) return first;
|
|
408
|
+
return first <= second ? first : second;
|
|
368
409
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { nearestTrackState } from '../../simulation/track/trackModel.js';
|
|
2
|
+
import { normalizeAngle } from '../../simulation/simMath.js';
|
|
3
|
+
import { offsetTrackPoint, pointAt, sampleIndexAtDistance } from '../../simulation/track/spatialQueries.js';
|
|
2
4
|
import { pitOverrideAllowedForCar } from '../../simulation/track/trackStatePolicy.js';
|
|
3
5
|
import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
|
|
4
6
|
import {
|
|
@@ -12,13 +14,18 @@ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
|
|
|
12
14
|
import { degreesToRadians, getCarRayOrigin, getCarRayVector, pointOnRay } from './rayGeometry.js';
|
|
13
15
|
import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
|
|
14
16
|
|
|
17
|
+
const RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE = metersToSimUnits(1);
|
|
18
|
+
const RAY_TRACK_STATE_REUSE_PROGRESS_TOLERANCE = metersToSimUnits(0.25);
|
|
19
|
+
const RAY_TRACK_STATE_REUSE_HEADING_TOLERANCE = 0.02;
|
|
20
|
+
|
|
15
21
|
export function createTrackRayContext(car, snapshot, origin, precision = 'debug') {
|
|
16
22
|
if (!Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
|
|
17
23
|
return { origin, originState: null, precision };
|
|
18
24
|
}
|
|
25
|
+
const reusedOriginState = reusableRayOriginState(snapshot.track, car, origin);
|
|
19
26
|
return {
|
|
20
27
|
origin,
|
|
21
|
-
originState: nearestRayTrackState(snapshot.track, car, origin, car.progress, precision),
|
|
28
|
+
originState: reusedOriginState ?? nearestRayTrackState(snapshot.track, car, origin, car.progress, precision),
|
|
22
29
|
precision,
|
|
23
30
|
};
|
|
24
31
|
}
|
|
@@ -296,6 +303,113 @@ function nearestRayTrackState(track, car, point, progressHint, precision = 'debu
|
|
|
296
303
|
});
|
|
297
304
|
}
|
|
298
305
|
|
|
306
|
+
function reusableRayOriginState(track, car, origin) {
|
|
307
|
+
const synthesizedState = synthesizedMainRoadRayOriginState(track, car, origin);
|
|
308
|
+
if (synthesizedState) return synthesizedState;
|
|
309
|
+
const trackState = car?.trackState;
|
|
310
|
+
if (!trackState || trackState.inPitLane) return null;
|
|
311
|
+
const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
|
|
312
|
+
if (!Number.isFinite(trackState.crossTrackError) || trackState.crossTrackError > mainRoadEdge) return null;
|
|
313
|
+
if (
|
|
314
|
+
typeof car?.trackStatePoseX === 'number' &&
|
|
315
|
+
typeof car?.trackStatePoseY === 'number' &&
|
|
316
|
+
typeof car?.trackStatePoseHeading === 'number'
|
|
317
|
+
) {
|
|
318
|
+
if (
|
|
319
|
+
car.trackStatePoseX !== car.x ||
|
|
320
|
+
car.trackStatePoseY !== car.y ||
|
|
321
|
+
car.trackStatePoseHeading !== car.heading
|
|
322
|
+
) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
if (!Number.isInteger(trackState.segmentId) && Number.isFinite(trackState.distance)) {
|
|
326
|
+
trackState.segmentId = sampleIndexAtDistance(track, trackState.distance);
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
if (!Number.isFinite(trackState.distance) || !Number.isFinite(trackState.signedOffset)) return null;
|
|
330
|
+
const base = pointAt(track, trackState.distance);
|
|
331
|
+
const projected = offsetTrackPoint(base, trackState.signedOffset);
|
|
332
|
+
const dx = (origin?.x ?? car?.x ?? 0) - projected.x;
|
|
333
|
+
const dy = (origin?.y ?? car?.y ?? 0) - projected.y;
|
|
334
|
+
if ((dx * dx) + (dy * dy) > RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE * RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (wrappedDistanceDelta(trackState.distance, car?.progress, track.length) > RAY_TRACK_STATE_REUSE_PROGRESS_TOLERANCE) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
if (Math.abs(normalizeAngle((trackState.heading ?? base.heading) - base.heading)) > RAY_TRACK_STATE_REUSE_HEADING_TOLERANCE) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const segmentId = trackState.segmentId ?? sampleIndexAtDistance(track, trackState.distance);
|
|
344
|
+
return {
|
|
345
|
+
segmentId,
|
|
346
|
+
x: base.x,
|
|
347
|
+
y: base.y,
|
|
348
|
+
distance: base.distance,
|
|
349
|
+
heading: base.heading,
|
|
350
|
+
normalX: base.normalX,
|
|
351
|
+
normalY: base.normalY,
|
|
352
|
+
curvature: base.curvature,
|
|
353
|
+
signedOffset: trackState.signedOffset,
|
|
354
|
+
crossTrackError: trackState.crossTrackError,
|
|
355
|
+
surface: trackState.surface ?? 'track',
|
|
356
|
+
onTrack: trackState.surface === 'track' || trackState.surface === 'kerb',
|
|
357
|
+
distanceSquared: trackState.distanceSquared,
|
|
358
|
+
inPitLane: false,
|
|
359
|
+
pitLanePart: null,
|
|
360
|
+
pitBoxId: null,
|
|
361
|
+
mainTrackSignedOffset: trackState.mainTrackSignedOffset,
|
|
362
|
+
mainTrackCrossTrackError: trackState.mainTrackCrossTrackError,
|
|
363
|
+
pitLaneCrossTrackError: trackState.pitLaneCrossTrackError,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return trackState;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function synthesizedMainRoadRayOriginState(track, car, origin) {
|
|
370
|
+
if (!Number.isFinite(car?.progress) || !Number.isFinite(car?.signedOffset)) return null;
|
|
371
|
+
if (car?.inPitLane || car?.pitLanePart) return null;
|
|
372
|
+
const crossTrackError = Math.abs(car.signedOffset);
|
|
373
|
+
const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
|
|
374
|
+
if (crossTrackError > mainRoadEdge) return null;
|
|
375
|
+
const base = pointAt(track, car.progress);
|
|
376
|
+
const projected = offsetTrackPoint(base, car.signedOffset);
|
|
377
|
+
const dx = (origin?.x ?? car?.x ?? 0) - projected.x;
|
|
378
|
+
const dy = (origin?.y ?? car?.y ?? 0) - projected.y;
|
|
379
|
+
if ((dx * dx) + (dy * dy) > RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE * RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const segmentId = car?.trackState?.segmentId ?? sampleIndexAtDistance(track, car.progress);
|
|
383
|
+
return {
|
|
384
|
+
segmentId,
|
|
385
|
+
x: base.x,
|
|
386
|
+
y: base.y,
|
|
387
|
+
distance: base.distance,
|
|
388
|
+
heading: base.heading,
|
|
389
|
+
normalX: base.normalX,
|
|
390
|
+
normalY: base.normalY,
|
|
391
|
+
curvature: base.curvature,
|
|
392
|
+
signedOffset: car.signedOffset,
|
|
393
|
+
crossTrackError,
|
|
394
|
+
surface: crossTrackError <= track.width / 2 ? 'track' : 'kerb',
|
|
395
|
+
onTrack: true,
|
|
396
|
+
distanceSquared: car?.trackState?.distanceSquared,
|
|
397
|
+
inPitLane: false,
|
|
398
|
+
pitLanePart: null,
|
|
399
|
+
pitBoxId: null,
|
|
400
|
+
mainTrackSignedOffset: car?.trackState?.mainTrackSignedOffset,
|
|
401
|
+
mainTrackCrossTrackError: car?.trackState?.mainTrackCrossTrackError,
|
|
402
|
+
pitLaneCrossTrackError: car?.trackState?.pitLaneCrossTrackError,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function wrappedDistanceDelta(first, second, totalLength) {
|
|
407
|
+
if (!Number.isFinite(first) || !Number.isFinite(second)) return Infinity;
|
|
408
|
+
if (!Number.isFinite(totalLength) || totalLength <= 0) return Math.abs(first - second);
|
|
409
|
+
const delta = Math.abs(first - second);
|
|
410
|
+
return Math.min(delta, totalLength - delta);
|
|
411
|
+
}
|
|
412
|
+
|
|
299
413
|
function refineStepsForPrecision(precision) {
|
|
300
414
|
return precision === 'debug' ? TRACK_RAY_REFINE_STEPS : DRIVER_RAY_REFINE_STEPS;
|
|
301
415
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -337,6 +337,32 @@ export interface F1SimulatorTheme extends Omit<F1SimulatorThemePackage, 'compone
|
|
|
337
337
|
>;
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
export type ResolvedPaddockThemeMode = Exclude<PaddockThemeMode, 'system'>;
|
|
341
|
+
|
|
342
|
+
export interface ResolvedPaddockThemePackage {
|
|
343
|
+
tokens: Record<ResolvedPaddockThemeMode, F1SimulatorThemeTokens>;
|
|
344
|
+
components: Record<string, Partial<Record<string, PaddockThemeTokenName>>>;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface ResolvedPaddockTheme {
|
|
348
|
+
mode: PaddockThemeMode;
|
|
349
|
+
activeMode: ResolvedPaddockThemeMode;
|
|
350
|
+
use: string;
|
|
351
|
+
tokens: Record<ResolvedPaddockThemeMode, F1SimulatorThemeTokens>;
|
|
352
|
+
activeTokens: F1SimulatorThemeTokens;
|
|
353
|
+
components: Record<string, Partial<Record<string, PaddockThemeTokenName>>>;
|
|
354
|
+
themes: Record<string, ResolvedPaddockThemePackage>;
|
|
355
|
+
componentThemes: Record<string, PaddockThemeSelector>;
|
|
356
|
+
teamThemes: Record<string, PaddockThemeSelector>;
|
|
357
|
+
light: F1SimulatorThemeTokens;
|
|
358
|
+
dark: F1SimulatorThemeTokens;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface PaddockThemeSyncOptions {
|
|
362
|
+
attribute?: string;
|
|
363
|
+
map?: Record<string, PaddockThemeMode | F1SimulatorTheme>;
|
|
364
|
+
}
|
|
365
|
+
|
|
340
366
|
export interface F1SimulatorUiOptions {
|
|
341
367
|
layoutPreset?: LayoutPreset;
|
|
342
368
|
cameraControls?: CameraControlsMode;
|
|
@@ -1000,6 +1026,10 @@ export interface F1MountedSimulator {
|
|
|
1000
1026
|
readonly expert: F1SimulatorExpertApi | null;
|
|
1001
1027
|
destroy(): void;
|
|
1002
1028
|
restart(nextOptions?: F1SimulatorRestartOptions): void;
|
|
1029
|
+
setTheme(theme: F1SimulatorTheme): ResolvedPaddockTheme;
|
|
1030
|
+
setThemeMode(mode: PaddockThemeMode): ResolvedPaddockTheme;
|
|
1031
|
+
getTheme(): ResolvedPaddockTheme;
|
|
1032
|
+
syncThemeFrom(source: Element, options?: PaddockThemeSyncOptions): () => void;
|
|
1003
1033
|
selectDriver(driverId: string): void;
|
|
1004
1034
|
setSafetyCarDeployed(deployed: boolean): void;
|
|
1005
1035
|
setRedFlagDeployed(deployed: boolean): void;
|
|
@@ -1040,6 +1070,10 @@ export interface PaddockSimulatorController {
|
|
|
1040
1070
|
start(): Promise<PaddockSimulatorController>;
|
|
1041
1071
|
destroy(): void;
|
|
1042
1072
|
restart(nextOptions?: F1SimulatorRestartOptions): void;
|
|
1073
|
+
setTheme(theme: F1SimulatorTheme): ResolvedPaddockTheme;
|
|
1074
|
+
setThemeMode(mode: PaddockThemeMode): ResolvedPaddockTheme;
|
|
1075
|
+
getTheme(): ResolvedPaddockTheme;
|
|
1076
|
+
syncThemeFrom(source: Element, options?: PaddockThemeSyncOptions): () => void;
|
|
1043
1077
|
selectDriver(driverId: string): void;
|
|
1044
1078
|
setSafetyCarDeployed(deployed: boolean): void;
|
|
1045
1079
|
setRedFlagDeployed(deployed: boolean): void;
|
|
@@ -1062,10 +1096,13 @@ export interface PaddockSimulatorController {
|
|
|
1062
1096
|
export const CHAMPIONSHIP_ENTRY_BLUEPRINTS: ChampionshipEntryBlueprint[];
|
|
1063
1097
|
export const DEMO_PROJECT_DRIVERS: SimulatorDriver[];
|
|
1064
1098
|
export const DEFAULT_F1_SIMULATOR_ASSETS: Required<F1SimulatorAssets>;
|
|
1099
|
+
export const DEFAULT_PADDOCK_THEME: F1SimulatorTheme;
|
|
1065
1100
|
export const PADDOCK_SIMULATOR_PRESETS: Record<PaddockPresetName, {
|
|
1066
1101
|
ui?: F1SimulatorUiOptions;
|
|
1067
1102
|
theme?: F1SimulatorTheme;
|
|
1068
1103
|
}>;
|
|
1104
|
+
export const PADDOCK_THEME_CSS_VARIABLES: Record<PaddockThemeTokenName, string>;
|
|
1105
|
+
export const PADDOCK_THEME_TOKEN_KEYS: readonly PaddockThemeTokenName[];
|
|
1069
1106
|
|
|
1070
1107
|
export const REAL_F1_CAR_LENGTH_METERS: number;
|
|
1071
1108
|
export const SIM_UNITS_PER_METER: number;
|
|
@@ -1081,6 +1118,12 @@ export function normalizeSimulatorDrivers(
|
|
|
1081
1118
|
drivers: SimulatorDriver[],
|
|
1082
1119
|
options?: { entries?: ChampionshipEntryBlueprint[]; caller?: string },
|
|
1083
1120
|
): NormalizedSimulatorDriver[];
|
|
1121
|
+
export function resolvePaddockTheme(theme?: F1SimulatorTheme): ResolvedPaddockTheme;
|
|
1122
|
+
export function applyPaddockTheme(
|
|
1123
|
+
root: Element,
|
|
1124
|
+
theme?: F1SimulatorTheme | ResolvedPaddockTheme,
|
|
1125
|
+
context?: { selectedTeamId?: string | null },
|
|
1126
|
+
): void;
|
|
1084
1127
|
|
|
1085
1128
|
export function metersToSimUnits(meters: number): number;
|
|
1086
1129
|
export function simUnitsToMeters(simUnits: number): number;
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import './styles.css';
|
|
|
2
2
|
import { F1SimulatorApp } from './app/F1SimulatorApp.js';
|
|
3
3
|
import { resolveF1SimulatorOptions } from './config/defaultOptions.js';
|
|
4
4
|
import { mergeRestartOptions } from './config/restartOptions.js';
|
|
5
|
+
import { createThemeSync, resolveRuntimeThemeModeOptions, resolveRuntimeThemeOptions } from './api/runtimeTheme.js';
|
|
5
6
|
import { createF1SimulatorShell } from './ui/shellTemplate.js';
|
|
6
7
|
export {
|
|
7
8
|
createPaddockSimulator,
|
|
@@ -30,7 +31,14 @@ export {
|
|
|
30
31
|
} from './data/championship.js';
|
|
31
32
|
export { DEMO_PROJECT_DRIVERS } from './data/demoDrivers.js';
|
|
32
33
|
export { DEFAULT_F1_SIMULATOR_ASSETS } from './config/defaultAssets.js';
|
|
33
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
DEFAULT_PADDOCK_THEME,
|
|
36
|
+
PADDOCK_SIMULATOR_PRESETS,
|
|
37
|
+
PADDOCK_THEME_CSS_VARIABLES,
|
|
38
|
+
PADDOCK_THEME_TOKEN_KEYS,
|
|
39
|
+
applyPaddockTheme,
|
|
40
|
+
resolvePaddockTheme,
|
|
41
|
+
} from './config/defaultOptions.js';
|
|
34
42
|
export { createPaddockDriverControllerLoop } from './environment/controllerLoop.js';
|
|
35
43
|
export { normalizeSimulatorDrivers } from './data/normalizeDrivers.js';
|
|
36
44
|
export { createProceduralTrack } from './simulation/track/trackModel.js';
|
|
@@ -76,7 +84,7 @@ export async function mountF1Simulator(root, options = {}) {
|
|
|
76
84
|
return resolvedOptions;
|
|
77
85
|
};
|
|
78
86
|
|
|
79
|
-
|
|
87
|
+
const mountedSimulator = {
|
|
80
88
|
get expert() {
|
|
81
89
|
return app.expert ?? null;
|
|
82
90
|
},
|
|
@@ -89,6 +97,22 @@ export async function mountF1Simulator(root, options = {}) {
|
|
|
89
97
|
app.restart(nextResolvedOptions);
|
|
90
98
|
resolvedOptions = nextResolvedOptions;
|
|
91
99
|
},
|
|
100
|
+
setTheme(themeInput = {}) {
|
|
101
|
+
const nextResolvedOptions = resolveRuntimeThemeOptions(syncResolvedRuntimeOptions(), themeInput);
|
|
102
|
+
resolvedOptions = nextResolvedOptions;
|
|
103
|
+
return app.setTheme(nextResolvedOptions.theme);
|
|
104
|
+
},
|
|
105
|
+
setThemeMode(mode) {
|
|
106
|
+
const nextResolvedOptions = resolveRuntimeThemeModeOptions(syncResolvedRuntimeOptions(), mode);
|
|
107
|
+
resolvedOptions = nextResolvedOptions;
|
|
108
|
+
return app.setTheme(nextResolvedOptions.theme);
|
|
109
|
+
},
|
|
110
|
+
getTheme() {
|
|
111
|
+
return resolvedOptions.theme;
|
|
112
|
+
},
|
|
113
|
+
syncThemeFrom(source, options = {}) {
|
|
114
|
+
return createThemeSync(mountedSimulator, source, options);
|
|
115
|
+
},
|
|
92
116
|
selectDriver(driverId) {
|
|
93
117
|
app.selectCar(driverId, { focus: true });
|
|
94
118
|
},
|
|
@@ -142,4 +166,5 @@ export async function mountF1Simulator(root, options = {}) {
|
|
|
142
166
|
return app.getSnapshot();
|
|
143
167
|
},
|
|
144
168
|
};
|
|
169
|
+
return mountedSimulator;
|
|
145
170
|
}
|