@inventure71/paddockjs 2.0.1 → 3.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 +31 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +65 -15
- package/docs/architecture.md +17 -13
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +2 -2
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +96 -21
- package/docs/index.md +2 -1
- package/docs/learnings.md +6 -6
- package/docs/rules.md +7 -7
- package/docs/sense_contract.md +1 -1
- package/docs/system_specs.md +25 -18
- package/docs/training.md +13 -12
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +74 -4
- package/src/app/F1SimulatorApp.js +191 -28
- 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 +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +17 -3
- package/src/environment/index.d.ts +1 -2
- package/src/environment/options.js +1 -1
- package/src/environment/runtime.js +3 -11
- package/src/environment/scenarios.js +1 -1
- package/src/environment/sensors/surfaceRays.js +41 -33
- package/src/environment/sensors/trackRays.js +44 -49
- package/src/index.d.ts +110 -9
- package/src/index.js +25 -1
- 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/pitKinematics.js +2 -2
- package/src/simulation/pit/pitRouting.js +2 -4
- package/src/simulation/race/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/safetyCar.js +1 -1
- package/src/simulation/race/stalledDnf.js +1 -1
- package/src/simulation/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryIndex.js +180 -135
- package/src/simulation/track/trackStatePolicy.js +0 -1
- package/src/simulation/vehicle/contactResolution.js +3 -3
- package/src/simulation/vehicle/vehicleKinematics.js +2 -2
- package/src/simulation/vehicle/vehiclePhysics.js +3 -3
- package/src/simulation/vehicle/vehicleSnapshots.js +12 -1
- package/src/simulation/vehicle/vehicleState.js +1 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- 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
|
@@ -43,6 +43,7 @@ export function createTrackQueryIndex(track) {
|
|
|
43
43
|
const index = {
|
|
44
44
|
version: 1,
|
|
45
45
|
bands,
|
|
46
|
+
nearestGridExpansion: expansion,
|
|
46
47
|
centerline,
|
|
47
48
|
grid,
|
|
48
49
|
segmentGrid,
|
|
@@ -54,7 +55,7 @@ export function createTrackQueryIndex(track) {
|
|
|
54
55
|
return index;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
export function queryNearestTrackProjection(track, position, progressHint = null
|
|
58
|
+
export function queryNearestTrackProjection(track, position, progressHint = null) {
|
|
58
59
|
const index = track?.queryIndex;
|
|
59
60
|
if (!index?.centerline?.segmentCount) {
|
|
60
61
|
recordFallback(index, 'nearestFallbackReasons', 'missing-index');
|
|
@@ -66,8 +67,7 @@ export function queryNearestTrackProjection(track, position, progressHint = null
|
|
|
66
67
|
}
|
|
67
68
|
index.stats.nearestQueries += 1;
|
|
68
69
|
|
|
69
|
-
const
|
|
70
|
-
const { projection, path, reason } = bestNearestFromIndex(track, index, position, progressHint, mode, options);
|
|
70
|
+
const { projection, path, reason } = bestProjectionFromIndex(index, position, progressHint);
|
|
71
71
|
if (!projection) {
|
|
72
72
|
recordFallback(index, 'nearestFallbackReasons', reason ?? 'unknown');
|
|
73
73
|
return null;
|
|
@@ -77,169 +77,173 @@ export function queryNearestTrackProjection(track, position, progressHint = null
|
|
|
77
77
|
return projection;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function
|
|
81
|
-
return mode === 'sample'
|
|
82
|
-
? bestSampleFromIndex(track, index, position, progressHint, options)
|
|
83
|
-
: bestProjectionFromIndex(index, position, progressHint, options);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function bestProjectionFromIndex(index, position, progressHint, options) {
|
|
80
|
+
function bestProjectionFromIndex(index, position, progressHint) {
|
|
87
81
|
const hintedCandidateIds = Number.isFinite(progressHint)
|
|
88
82
|
? candidateIdsFromArcBuckets(index, progressHint, 2)
|
|
89
83
|
: [];
|
|
90
84
|
if (hintedCandidateIds.length) {
|
|
91
85
|
const hinted = bestProjectionFromCandidates(index.centerline, hintedCandidateIds, position, progressHint);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
if (hinted.projection) {
|
|
87
|
+
const exact = bestProjectionFromCellSearch(index, position, progressHint, hinted.projection);
|
|
88
|
+
if (exact.projection) {
|
|
89
|
+
const path = projectionMatches(hinted.projection, exact.projection)
|
|
90
|
+
? (hinted.reason === 'tie-resolved' ? 'arc-hint-tie-resolved' : 'arc-hint')
|
|
91
|
+
: 'arc-hint-refined';
|
|
92
|
+
return {
|
|
93
|
+
projection: exact.projection,
|
|
94
|
+
path: exact.reason === 'tie-resolved' && !path.endsWith('tie-resolved') ? `${path}-tie-resolved` : path,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
98
97
|
}
|
|
99
|
-
recordStat(index, 'nearestPaths', hinted.projection ? 'arc-hint-rejected-distance' : `arc-hint-${hinted.reason}`);
|
|
100
98
|
}
|
|
101
99
|
|
|
100
|
+
let local = { projection: null, path: null };
|
|
102
101
|
const gridCandidateIds = candidateIdsFromGrid(index, index.grid, position, GRID_NEIGHBOR_LIMIT);
|
|
103
102
|
const grid = bestProjectionFromCandidates(index.centerline, gridCandidateIds, position, progressHint);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
if (grid.projection) {
|
|
104
|
+
local = chooseBetterProjectionResult(local, grid, 'spatial-grid', progressHint);
|
|
105
|
+
if (grid.projection.distanceSquared <= index.nearestGridExpansion * index.nearestGridExpansion) {
|
|
106
|
+
return local;
|
|
108
107
|
}
|
|
109
|
-
|
|
110
|
-
}
|
|
108
|
+
}
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
: [];
|
|
119
|
-
if (hintedCandidateIds.length) {
|
|
120
|
-
const hinted = bestSampleFromCandidates(index, samples, sampleCount, hintedCandidateIds, position, progressHint);
|
|
121
|
-
const maxHintDistance = resolveHintMaxDistance(index, options);
|
|
122
|
-
if (hinted.sample && hinted.distanceSquared <= maxHintDistance * maxHintDistance) {
|
|
110
|
+
if (local.projection) {
|
|
111
|
+
const exact = bestProjectionFromCellSearch(index, position, progressHint, local.projection);
|
|
112
|
+
if (exact.projection) {
|
|
113
|
+
const path = projectionMatches(local.projection, exact.projection)
|
|
114
|
+
? local.path
|
|
115
|
+
: `${local.path}-refined`;
|
|
123
116
|
return {
|
|
124
|
-
projection:
|
|
125
|
-
path:
|
|
117
|
+
projection: exact.projection,
|
|
118
|
+
path: exact.reason === 'tie-resolved' && !path.endsWith('tie-resolved') ? `${path}-tie-resolved` : path,
|
|
126
119
|
};
|
|
127
120
|
}
|
|
128
|
-
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
projection: local.projection,
|
|
124
|
+
path: local.path,
|
|
125
|
+
};
|
|
129
126
|
}
|
|
130
127
|
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
return
|
|
128
|
+
const exact = bestProjectionFromCellSearch(index, position, progressHint);
|
|
129
|
+
const global = exact.projection ? exact : bestProjectionFromAllSegments(index.centerline, position, progressHint);
|
|
130
|
+
return global.projection
|
|
134
131
|
? {
|
|
135
|
-
projection:
|
|
136
|
-
path:
|
|
132
|
+
projection: global.projection,
|
|
133
|
+
path: exact.projection
|
|
134
|
+
? (global.reason === 'tie-resolved' ? 'exact-grid-tie-resolved' : 'exact-grid')
|
|
135
|
+
: (global.reason === 'tie-resolved' ? 'global-index-tie-resolved' : 'global-index'),
|
|
137
136
|
}
|
|
138
|
-
: { projection: null, reason: `
|
|
137
|
+
: { projection: null, reason: `global-index-${global.reason}` };
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
function
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
return
|
|
140
|
+
function chooseBetterProjectionResult(current, candidate, source, preferredDistance) {
|
|
141
|
+
if (!candidate.projection) return current;
|
|
142
|
+
const path = candidate.reason === 'tie-resolved' ? `${source}-tie-resolved` : source;
|
|
143
|
+
if (!current.projection) return { projection: candidate.projection, path };
|
|
144
|
+
if (candidate.projection.distanceSquared < current.projection.distanceSquared - AMBIGUOUS_DISTANCE_EPSILON) {
|
|
145
|
+
return { projection: candidate.projection, path };
|
|
146
|
+
}
|
|
147
|
+
if (Math.abs(candidate.projection.distanceSquared - current.projection.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON) {
|
|
148
|
+
const candidateTie = projectionTieScore(candidate.projection, preferredDistance);
|
|
149
|
+
const currentTie = projectionTieScore(current.projection, preferredDistance);
|
|
150
|
+
if (projectionTieBeats(candidate.projection, candidateTie, current.projection, currentTie)) {
|
|
151
|
+
return { projection: candidate.projection, path: `${path}-tie-resolved` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return current;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const sampleMarks = ensureScratchArray(scratch, 'sampleMarks', sampleCount);
|
|
151
|
-
const sampleEpoch = nextScratchEpoch(scratch, 'sampleEpoch', sampleMarks);
|
|
157
|
+
function bestProjectionFromCellSearch(index, position, preferredDistance, initialProjection = null) {
|
|
158
|
+
const grid = index.segmentGrid ?? index.grid;
|
|
159
|
+
const center = gridCellForPoint(grid, position, true);
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
const scratch = ensureQueryScratch(index);
|
|
162
|
+
const segmentMarks = ensureScratchArray(scratch, 'exactSegmentMarks', index.centerline.segmentCount);
|
|
163
|
+
const cellMarks = ensureScratchArray(scratch, 'exactCellMarks', grid.columns * grid.rows);
|
|
164
|
+
const segmentEpoch = nextScratchEpoch(scratch, 'exactSegmentEpoch', segmentMarks);
|
|
165
|
+
const cellEpoch = nextScratchEpoch(scratch, 'exactCellEpoch', cellMarks);
|
|
166
|
+
const maxRadius = Math.max(grid.columns, grid.rows);
|
|
167
|
+
let best = initialProjection;
|
|
168
|
+
let bestTieScore = best ? projectionTieScore(best, preferredDistance) : Infinity;
|
|
156
169
|
let secondBestDistance = Infinity;
|
|
157
170
|
let tieResolved = false;
|
|
158
|
-
for (const segmentId of candidateSegmentIds) {
|
|
159
|
-
const normalized = ((segmentId % sampleCount) + sampleCount) % sampleCount;
|
|
160
|
-
const adjacent = (normalized + 1) % sampleCount;
|
|
161
|
-
if (sampleMarks[normalized] !== sampleEpoch) {
|
|
162
|
-
sampleMarks[normalized] = sampleEpoch;
|
|
163
|
-
({ bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved } = considerSampleCandidate({
|
|
164
|
-
samples,
|
|
165
|
-
sampleId: normalized,
|
|
166
|
-
position,
|
|
167
|
-
preferredDistance,
|
|
168
|
-
bestSample,
|
|
169
|
-
bestDistanceSquared,
|
|
170
|
-
bestTieScore,
|
|
171
|
-
secondBestDistance,
|
|
172
|
-
tieResolved,
|
|
173
|
-
}));
|
|
174
|
-
}
|
|
175
|
-
if (sampleMarks[adjacent] !== sampleEpoch) {
|
|
176
|
-
sampleMarks[adjacent] = sampleEpoch;
|
|
177
|
-
({ bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved } = considerSampleCandidate({
|
|
178
|
-
samples,
|
|
179
|
-
sampleId: adjacent,
|
|
180
|
-
position,
|
|
181
|
-
preferredDistance,
|
|
182
|
-
bestSample,
|
|
183
|
-
bestDistanceSquared,
|
|
184
|
-
bestTieScore,
|
|
185
|
-
secondBestDistance,
|
|
186
|
-
tieResolved,
|
|
187
|
-
}));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
171
|
|
|
191
|
-
|
|
172
|
+
for (let radius = 0; radius <= maxRadius; radius += 1) {
|
|
173
|
+
let ringCanImprove = !best;
|
|
174
|
+
|
|
175
|
+
forEachRingCell(grid, center, radius, (cellIndex, row, column) => {
|
|
176
|
+
if (cellMarks[cellIndex] === cellEpoch) return;
|
|
177
|
+
cellMarks[cellIndex] = cellEpoch;
|
|
178
|
+
const lowerBound = cellDistanceSquared(grid, row, column, position);
|
|
179
|
+
if (best && lowerBound > best.distanceSquared + AMBIGUOUS_DISTANCE_EPSILON) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
ringCanImprove = true;
|
|
183
|
+
const cell = grid.cells[cellIndex];
|
|
184
|
+
if (!cell) return;
|
|
185
|
+
for (const segmentId of cell) {
|
|
186
|
+
if (segmentMarks[segmentId] === segmentEpoch) continue;
|
|
187
|
+
segmentMarks[segmentId] = segmentEpoch;
|
|
188
|
+
const projection = projectIndexedSegment(index.centerline, segmentId, position);
|
|
189
|
+
const tieScore = projectionTieScore(projection, preferredDistance);
|
|
190
|
+
if (!best || projection.distanceSquared < best.distanceSquared - AMBIGUOUS_DISTANCE_EPSILON) {
|
|
191
|
+
secondBestDistance = best?.distanceSquared ?? Infinity;
|
|
192
|
+
best = projection;
|
|
193
|
+
bestTieScore = tieScore;
|
|
194
|
+
} else if (Math.abs(projection.distanceSquared - best.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON) {
|
|
195
|
+
tieResolved = true;
|
|
196
|
+
secondBestDistance = Math.min(secondBestDistance, projection.distanceSquared);
|
|
197
|
+
if (projectionTieBeats(projection, tieScore, best, bestTieScore)) {
|
|
198
|
+
best = projection;
|
|
199
|
+
bestTieScore = tieScore;
|
|
200
|
+
}
|
|
201
|
+
} else if (projection.distanceSquared < secondBestDistance) {
|
|
202
|
+
secondBestDistance = projection.distanceSquared;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
if (best && !ringCanImprove) break;
|
|
207
|
+
}
|
|
192
208
|
|
|
209
|
+
if (!best) return { projection: null, reason: 'no-candidates' };
|
|
193
210
|
const ambiguous = Number.isFinite(secondBestDistance) &&
|
|
194
|
-
Math.abs(secondBestDistance -
|
|
195
|
-
return {
|
|
196
|
-
sample: bestSample,
|
|
197
|
-
distanceSquared: bestDistanceSquared,
|
|
198
|
-
reason: ambiguous || tieResolved ? 'tie-resolved' : 'ok',
|
|
199
|
-
};
|
|
211
|
+
Math.abs(secondBestDistance - best.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON;
|
|
212
|
+
return { projection: best, reason: ambiguous || tieResolved ? 'tie-resolved' : 'ok' };
|
|
200
213
|
}
|
|
201
214
|
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
secondBestDistance = bestDistanceSquared;
|
|
223
|
-
bestSample = sample;
|
|
224
|
-
bestDistanceSquared = distanceSquared;
|
|
225
|
-
bestTieScore = tieScore;
|
|
226
|
-
} else if (Math.abs(distanceSquared - bestDistanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON) {
|
|
227
|
-
tieResolved = true;
|
|
228
|
-
secondBestDistance = Math.min(secondBestDistance, distanceSquared);
|
|
229
|
-
if (tieScore < bestTieScore) {
|
|
230
|
-
bestSample = sample;
|
|
231
|
-
bestDistanceSquared = distanceSquared;
|
|
232
|
-
bestTieScore = tieScore;
|
|
215
|
+
function projectionMatches(first, second) {
|
|
216
|
+
return first.segmentId === second.segmentId &&
|
|
217
|
+
Math.abs(first.distanceSquared - second.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function forEachRingCell(grid, center, radius, visitor) {
|
|
221
|
+
const minRow = Math.max(0, center.row - radius);
|
|
222
|
+
const maxRow = Math.min(grid.rows - 1, center.row + radius);
|
|
223
|
+
const minColumn = Math.max(0, center.column - radius);
|
|
224
|
+
const maxColumn = Math.min(grid.columns - 1, center.column + radius);
|
|
225
|
+
for (let row = minRow; row <= maxRow; row += 1) {
|
|
226
|
+
for (let column = minColumn; column <= maxColumn; column += 1) {
|
|
227
|
+
if (
|
|
228
|
+
radius > 0 &&
|
|
229
|
+
row !== minRow &&
|
|
230
|
+
row !== maxRow &&
|
|
231
|
+
column !== minColumn &&
|
|
232
|
+
column !== maxColumn
|
|
233
|
+
) continue;
|
|
234
|
+
visitor(row * grid.columns + column, row, column);
|
|
233
235
|
}
|
|
234
|
-
} else if (distanceSquared < secondBestDistance) {
|
|
235
|
-
secondBestDistance = distanceSquared;
|
|
236
236
|
}
|
|
237
|
-
return { bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved };
|
|
238
237
|
}
|
|
239
238
|
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
function cellDistanceSquared(grid, row, column, position) {
|
|
240
|
+
const minX = grid.bounds.minX + column * grid.cellSize;
|
|
241
|
+
const minY = grid.bounds.minY + row * grid.cellSize;
|
|
242
|
+
const maxX = Math.min(grid.bounds.maxX, minX + grid.cellSize);
|
|
243
|
+
const maxY = Math.min(grid.bounds.maxY, minY + grid.cellSize);
|
|
244
|
+
const dx = position.x < minX ? minX - position.x : position.x > maxX ? position.x - maxX : 0;
|
|
245
|
+
const dy = position.y < minY ? minY - position.y : position.y > maxY ? position.y - maxY : 0;
|
|
246
|
+
return dx * dx + dy * dy;
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
function bestProjectionFromCandidates(centerline, candidateIds, position, preferredDistance = null) {
|
|
@@ -258,7 +262,7 @@ function bestProjectionFromCandidates(centerline, candidateIds, position, prefer
|
|
|
258
262
|
} else if (Math.abs(projection.distanceSquared - best.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON) {
|
|
259
263
|
tieResolved = true;
|
|
260
264
|
secondBestDistance = Math.min(secondBestDistance, projection.distanceSquared);
|
|
261
|
-
if (tieScore
|
|
265
|
+
if (projectionTieBeats(projection, tieScore, best, bestTieScore)) {
|
|
262
266
|
best = projection;
|
|
263
267
|
bestTieScore = tieScore;
|
|
264
268
|
}
|
|
@@ -274,11 +278,50 @@ function bestProjectionFromCandidates(centerline, candidateIds, position, prefer
|
|
|
274
278
|
return { projection: best, reason: ambiguous || tieResolved ? 'tie-resolved' : 'ok' };
|
|
275
279
|
}
|
|
276
280
|
|
|
281
|
+
function bestProjectionFromAllSegments(centerline, position, preferredDistance = null) {
|
|
282
|
+
const segmentCount = centerline?.segmentCount ?? 0;
|
|
283
|
+
if (segmentCount <= 0) return { projection: null, reason: 'no-candidates' };
|
|
284
|
+
let best = null;
|
|
285
|
+
let bestTieScore = Infinity;
|
|
286
|
+
let secondBestDistance = Infinity;
|
|
287
|
+
let tieResolved = false;
|
|
288
|
+
|
|
289
|
+
for (let segmentId = 0; segmentId < segmentCount; segmentId += 1) {
|
|
290
|
+
const projection = projectIndexedSegment(centerline, segmentId, position);
|
|
291
|
+
const tieScore = projectionTieScore(projection, preferredDistance);
|
|
292
|
+
if (!best || projection.distanceSquared < best.distanceSquared - AMBIGUOUS_DISTANCE_EPSILON) {
|
|
293
|
+
secondBestDistance = best?.distanceSquared ?? Infinity;
|
|
294
|
+
best = projection;
|
|
295
|
+
bestTieScore = tieScore;
|
|
296
|
+
} else if (Math.abs(projection.distanceSquared - best.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON) {
|
|
297
|
+
tieResolved = true;
|
|
298
|
+
secondBestDistance = Math.min(secondBestDistance, projection.distanceSquared);
|
|
299
|
+
if (projectionTieBeats(projection, tieScore, best, bestTieScore)) {
|
|
300
|
+
best = projection;
|
|
301
|
+
bestTieScore = tieScore;
|
|
302
|
+
}
|
|
303
|
+
} else if (projection.distanceSquared < secondBestDistance) {
|
|
304
|
+
secondBestDistance = projection.distanceSquared;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!best) return { projection: null, reason: 'no-best' };
|
|
309
|
+
const ambiguous = Number.isFinite(secondBestDistance) &&
|
|
310
|
+
Math.abs(secondBestDistance - best.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON;
|
|
311
|
+
return { projection: best, reason: ambiguous || tieResolved ? 'tie-resolved' : 'ok' };
|
|
312
|
+
}
|
|
313
|
+
|
|
277
314
|
function projectionTieScore(projection, preferredDistance) {
|
|
278
315
|
if (!Number.isFinite(preferredDistance)) return projection.segmentId;
|
|
279
316
|
return Math.abs(projection.distance - preferredDistance);
|
|
280
317
|
}
|
|
281
318
|
|
|
319
|
+
function projectionTieBeats(candidate, candidateTieScore, current, currentTieScore) {
|
|
320
|
+
if (candidateTieScore < currentTieScore - AMBIGUOUS_DISTANCE_EPSILON) return true;
|
|
321
|
+
if (Math.abs(candidateTieScore - currentTieScore) > AMBIGUOUS_DISTANCE_EPSILON) return false;
|
|
322
|
+
return candidate.segmentId < current.segmentId;
|
|
323
|
+
}
|
|
324
|
+
|
|
282
325
|
export function queryNearbyTrackProjections(track, position, { neighborLimit = GRID_NEIGHBOR_LIMIT } = {}) {
|
|
283
326
|
const index = track?.queryIndex;
|
|
284
327
|
if (!index?.centerline?.segmentCount || !finitePoint(position)) return null;
|
|
@@ -364,13 +407,15 @@ export function attachTrackQueryIndex(track, queryIndex) {
|
|
|
364
407
|
return track;
|
|
365
408
|
}
|
|
366
409
|
|
|
367
|
-
export function forkTrackQueryIndex(sourceIndex) {
|
|
410
|
+
export function forkTrackQueryIndex(sourceIndex, runtimeTrack = null) {
|
|
368
411
|
if (!sourceIndex || typeof sourceIndex !== 'object') return null;
|
|
369
|
-
|
|
412
|
+
const index = {
|
|
370
413
|
...sourceIndex,
|
|
414
|
+
pit: runtimeTrack ? createPitQueryIndex(runtimeTrack, sourceIndex.grid?.cellSize ?? DEFAULT_GRID_CELL_SIZE) : sourceIndex.pit,
|
|
371
415
|
stats: createQueryStats(),
|
|
372
|
-
queryScratch: createQueryScratch(sourceIndex),
|
|
373
416
|
};
|
|
417
|
+
index.queryScratch = createQueryScratch(index);
|
|
418
|
+
return index;
|
|
374
419
|
}
|
|
375
420
|
|
|
376
421
|
export function resetTrackQueryStats(track) {
|
|
@@ -17,7 +17,6 @@ export function nearestTrackStateForCar(
|
|
|
17
17
|
) {
|
|
18
18
|
const resolvedAllowPitOverride = options.allowPitOverride ?? pitOverrideAllowedForCar(car);
|
|
19
19
|
return nearestTrackState(track, position, progressHint, {
|
|
20
|
-
indexMode: options.indexMode ?? 'legacy',
|
|
21
20
|
...options,
|
|
22
21
|
allowPitOverride: resolvedAllowPitOverride,
|
|
23
22
|
});
|
|
@@ -79,8 +79,8 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
if (oneCarFixed) {
|
|
82
|
-
if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode === '
|
|
83
|
-
if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode === '
|
|
82
|
+
if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode === 'advanced');
|
|
83
|
+
if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode === 'advanced');
|
|
84
84
|
} else {
|
|
85
85
|
applyContactVelocityResponse(sim, first, second, collision.axis);
|
|
86
86
|
}
|
|
@@ -122,7 +122,7 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
122
122
|
|
|
123
123
|
export function applyContactVelocityResponse(sim, first, second, axis) {
|
|
124
124
|
if (
|
|
125
|
-
sim.physicsMode === '
|
|
125
|
+
sim.physicsMode === 'advanced' ||
|
|
126
126
|
Number.isFinite(first.velocityX) ||
|
|
127
127
|
Number.isFinite(second.velocityX)
|
|
128
128
|
) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export function freezeVehicleMotion(car, {
|
|
2
2
|
brake = 1,
|
|
3
3
|
clearManualControls = false,
|
|
4
|
-
physicsMode = '
|
|
4
|
+
physicsMode = 'arcade',
|
|
5
5
|
stabilityState = car.stabilityState ?? 'stable',
|
|
6
6
|
} = {}) {
|
|
7
7
|
car.speed = 0;
|
|
8
|
-
if (physicsMode === '
|
|
8
|
+
if (physicsMode === 'advanced') {
|
|
9
9
|
car.velocityX = 0;
|
|
10
10
|
car.velocityY = 0;
|
|
11
11
|
} else {
|
|
@@ -11,7 +11,7 @@ import { VEHICLE_GEOMETRY, getCarCorners, vehicleAxes } from './vehicleGeometry.
|
|
|
11
11
|
|
|
12
12
|
const G = 9.80665;
|
|
13
13
|
|
|
14
|
-
export const PHYSICS_MODES = ['arcade', '
|
|
14
|
+
export const PHYSICS_MODES = ['arcade', 'advanced'];
|
|
15
15
|
export const DEFAULT_PHYSICS_MODE = 'arcade';
|
|
16
16
|
|
|
17
17
|
export const VEHICLE_LIMITS = {
|
|
@@ -50,7 +50,7 @@ const SIMULATOR_SURFACE_MODEL = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export function normalizePhysicsMode(value) {
|
|
53
|
-
return value === '
|
|
53
|
+
return value === 'advanced' ? 'advanced' : DEFAULT_PHYSICS_MODE;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function accelerationLimit(speed, surfaceGrip) {
|
|
@@ -111,7 +111,7 @@ function setPhysicsTelemetry(car, {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export function integrateVehiclePhysics(car, controls, dt, options = {}) {
|
|
114
|
-
if (normalizePhysicsMode(options.physicsMode) === '
|
|
114
|
+
if (normalizePhysicsMode(options.physicsMode) === 'advanced') {
|
|
115
115
|
return integrateSimulatorVehiclePhysics(car, controls, dt, options);
|
|
116
116
|
}
|
|
117
117
|
return integrateArcadeVehiclePhysics(car, controls, dt, options);
|
|
@@ -17,7 +17,7 @@ function serializeAppliedControls(car, dependencies) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function serializeVelocityComponent(car, component, dependencies) {
|
|
20
|
-
if (dependencies.physicsMode !== '
|
|
20
|
+
if (dependencies.physicsMode !== 'advanced') return null;
|
|
21
21
|
return finiteOrNull(car[component]);
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -141,6 +141,17 @@ export function serializeCar(car, rank, penaltySeconds = 0, dependencies) {
|
|
|
141
141
|
signedOffset: car.trackState?.signedOffset ?? 0,
|
|
142
142
|
crossTrackError: car.trackState?.crossTrackError ?? 0,
|
|
143
143
|
surface: car.trackState?.surface ?? 'track',
|
|
144
|
+
trackState: car.trackState ? {
|
|
145
|
+
distance: car.trackState.distance,
|
|
146
|
+
signedOffset: car.trackState.signedOffset,
|
|
147
|
+
crossTrackError: car.trackState.crossTrackError,
|
|
148
|
+
surface: car.trackState.surface,
|
|
149
|
+
inPitLane: Boolean(car.trackState.inPitLane),
|
|
150
|
+
pitLanePart: car.trackState.pitLanePart ?? null,
|
|
151
|
+
pitBoxId: car.trackState.pitBoxId ?? null,
|
|
152
|
+
curvature: car.trackState.curvature ?? 0,
|
|
153
|
+
heading: car.trackState.heading,
|
|
154
|
+
} : null,
|
|
144
155
|
wheels: serializeWheels(car.wheelStates),
|
|
145
156
|
inPitLane: Boolean(car.trackState?.inPitLane),
|
|
146
157
|
pitLanePart: car.trackState?.pitLanePart ?? null,
|
|
@@ -164,7 +164,7 @@ export function applyExternalCarState(car, partial, context) {
|
|
|
164
164
|
}
|
|
165
165
|
car.speed = clamp(Number.isFinite(car.speed) ? car.speed : 0, 0, VEHICLE_LIMITS.maxSpeed);
|
|
166
166
|
car.heading = normalizeAngle(Number.isFinite(car.heading) ? car.heading : 0);
|
|
167
|
-
syncExternalVelocityState(car, nextPartial, physicsMode === '
|
|
167
|
+
syncExternalVelocityState(car, nextPartial, physicsMode === 'advanced');
|
|
168
168
|
const centerState = nearestTrackStateForCar(track, car, car, car.progress ?? car.raceDistance);
|
|
169
169
|
applyWheelSurfaceState(car, track, { centerState });
|
|
170
170
|
if (
|
|
@@ -116,7 +116,6 @@ function createWarmupFingerprint(options, { surface, steps }) {
|
|
|
116
116
|
trackGeneration: options.trackGeneration ?? null,
|
|
117
117
|
track: summarizeTrack(options.track),
|
|
118
118
|
physicsMode: options.physicsMode ?? null,
|
|
119
|
-
trackQueryIndex: options.trackQueryIndex !== false,
|
|
120
119
|
rules: options.rules ?? null,
|
|
121
120
|
participantInteractions: options.participantInteractions ?? null,
|
|
122
121
|
replayGhosts: summarizeReplayGhosts(options.replayGhosts),
|