@inventure71/paddockjs 2.0.0 → 2.0.1
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 +6 -0
- package/INSTALL_AND_UPDATE.md +1 -1
- package/README.md +30 -5
- package/docs/architecture.md +15 -9
- package/docs/concepts.md +6 -4
- package/docs/data_contract.md +28 -15
- package/docs/data_model.md +1 -1
- package/docs/index.md +9 -0
- package/docs/rules.md +37 -7
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +56 -7
- package/docs/training.md +16 -10
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +2 -33
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +1 -42
- package/src/config/restartOptions.js +34 -0
- package/src/environment/controllerLoop.js +17 -2
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +16 -2
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +62 -14
- package/src/environment/scenarios.js +22 -3
- package/src/environment/sensors/index.js +3 -3
- package/src/environment/sensors/opponentRadar.js +13 -3
- package/src/environment/sensors/sensorTargets.js +8 -2
- package/src/environment/sensors/surfaceRays.js +90 -26
- package/src/environment/sensors/trackRays.js +185 -22
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -32
- package/src/simulation/pit/pitFlow.js +5 -0
- package/src/simulation/pit/pitKinematics.js +24 -0
- package/src/simulation/pit/pitQueue.js +5 -0
- package/src/simulation/pit/pitRouteMovement.js +7 -0
- package/src/simulation/pit/pitService.js +2 -0
- package/src/simulation/pit/pitTireService.js +2 -0
- package/src/simulation/race/raceLifecycle.js +2 -6
- package/src/simulation/race/raceProgress.js +6 -1
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/redFlag.js +3 -3
- package/src/simulation/race/safetyCar.js +10 -4
- package/src/simulation/race/stalledDnf.js +8 -17
- package/src/simulation/raceSimulation.js +1 -0
- package/src/simulation/rules/rulesReview.js +3 -0
- package/src/simulation/snapshots/raceSnapshots.js +1 -0
- package/src/simulation/track/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +16 -302
- package/src/simulation/track/trackQueryPitIndex.js +140 -0
- package/src/simulation/track/trackQueryScratch.js +46 -0
- package/src/simulation/track/trackQueryStats.js +28 -0
- package/src/simulation/vehicle/contactResolution.js +67 -2
- package/src/simulation/vehicle/runoffResponse.js +11 -12
- package/src/simulation/vehicle/vehicleKinematics.js +37 -0
- package/src/simulation/vehicle/vehiclePhysics.js +2 -1
- package/src/simulation/vehicle/vehicleSnapshots.js +11 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -1
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { clamp, wrapDistance } from '../simMath.js';
|
|
2
2
|
import { metersToSimUnits } from '../units.js';
|
|
3
|
+
import {
|
|
4
|
+
candidateIdsFromGrid,
|
|
5
|
+
candidateIdsFromGridBounds,
|
|
6
|
+
createSpatialGrid,
|
|
7
|
+
gridCellForPoint,
|
|
8
|
+
insertIdIntoGridBounds,
|
|
9
|
+
} from './trackQueryGrid.js';
|
|
10
|
+
import { createPitQueryIndex } from './trackQueryPitIndex.js';
|
|
11
|
+
import { createQueryScratch, ensureQueryScratch, ensureScratchArray, nextScratchEpoch } from './trackQueryScratch.js';
|
|
12
|
+
import { createQueryStats, recordFallback, recordStat } from './trackQueryStats.js';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
queryPitBoxCandidates,
|
|
16
|
+
queryPitRoadSegmentCandidates,
|
|
17
|
+
queryPitRoadSegmentCandidatesByRoute,
|
|
18
|
+
} from './trackQueryPitIndex.js';
|
|
3
19
|
|
|
4
20
|
const DEFAULT_GRID_CELL_SIZE = metersToSimUnits(32);
|
|
5
21
|
const GRID_NEIGHBOR_LIMIT = 2;
|
|
@@ -263,59 +279,6 @@ function projectionTieScore(projection, preferredDistance) {
|
|
|
263
279
|
return Math.abs(projection.distance - preferredDistance);
|
|
264
280
|
}
|
|
265
281
|
|
|
266
|
-
export function queryPitBoxCandidates(track, position) {
|
|
267
|
-
const index = track?.queryIndex;
|
|
268
|
-
const pit = index?.pit;
|
|
269
|
-
if (!pit?.boxGrid) {
|
|
270
|
-
recordFallback(index, 'pitFallbackReasons', 'missing-box-index');
|
|
271
|
-
return null;
|
|
272
|
-
}
|
|
273
|
-
if (!finitePoint(position)) {
|
|
274
|
-
recordFallback(index, 'pitFallbackReasons', 'invalid-position');
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
index.stats.pitQueries += 1;
|
|
278
|
-
const ids = candidateIdsFromGrid(index, pit.boxGrid, position, 1);
|
|
279
|
-
if (!ids.length) {
|
|
280
|
-
recordStat(index, 'pitPaths', 'box-grid-miss');
|
|
281
|
-
return [];
|
|
282
|
-
}
|
|
283
|
-
recordStat(index, 'pitPaths', 'box-grid-hit');
|
|
284
|
-
return ids.map((id) => pit.boxCandidates[id]).filter(Boolean);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export function queryPitRoadSegmentCandidates(track, routeId, position) {
|
|
288
|
-
const candidatesByRoute = queryPitRoadSegmentCandidatesByRoute(track, position);
|
|
289
|
-
return candidatesByRoute?.[routeId] ?? candidatesByRoute;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export function queryPitRoadSegmentCandidatesByRoute(track, position) {
|
|
293
|
-
const index = track?.queryIndex;
|
|
294
|
-
const pit = index?.pit;
|
|
295
|
-
if (!pit?.roadGrid) {
|
|
296
|
-
recordFallback(index, 'pitFallbackReasons', 'missing-road-index');
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
if (!finitePoint(position)) {
|
|
300
|
-
recordFallback(index, 'pitFallbackReasons', 'invalid-position');
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
index.stats.pitQueries += 1;
|
|
304
|
-
const ids = candidateIdsFromGrid(index, pit.roadGrid, position, 1);
|
|
305
|
-
if (!ids.length) {
|
|
306
|
-
recordStat(index, 'pitPaths', 'road-grid-miss');
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
const candidatesByRoute = { entry: [], main: [], working: [], exit: [] };
|
|
310
|
-
ids.forEach((id) => {
|
|
311
|
-
const segment = pit.roadSegments[id];
|
|
312
|
-
if (!segment || !candidatesByRoute[segment.routeId]) return;
|
|
313
|
-
candidatesByRoute[segment.routeId].push(segment.segmentIndex);
|
|
314
|
-
});
|
|
315
|
-
recordStat(index, 'pitPaths', 'road-grid-hit');
|
|
316
|
-
return candidatesByRoute;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
282
|
export function queryNearbyTrackProjections(track, position, { neighborLimit = GRID_NEIGHBOR_LIMIT } = {}) {
|
|
320
283
|
const index = track?.queryIndex;
|
|
321
284
|
if (!index?.centerline?.segmentCount || !finitePoint(position)) return null;
|
|
@@ -422,35 +385,6 @@ export function snapshotTrackQueryStats(track) {
|
|
|
422
385
|
return stats ? JSON.parse(JSON.stringify(stats)) : null;
|
|
423
386
|
}
|
|
424
387
|
|
|
425
|
-
function createQueryStats() {
|
|
426
|
-
return {
|
|
427
|
-
nearestQueries: 0,
|
|
428
|
-
nearestFallbacks: 0,
|
|
429
|
-
nearestPaths: {},
|
|
430
|
-
nearestFallbackReasons: {
|
|
431
|
-
'spatial-grid-no-candidates': 0,
|
|
432
|
-
},
|
|
433
|
-
pitQueries: 0,
|
|
434
|
-
pitFallbacks: 0,
|
|
435
|
-
pitPaths: {},
|
|
436
|
-
pitFallbackReasons: {},
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function recordFallback(index, bucket, reason) {
|
|
441
|
-
if (!index?.stats) return;
|
|
442
|
-
if (bucket.startsWith('nearest')) index.stats.nearestFallbacks += 1;
|
|
443
|
-
if (bucket.startsWith('pit')) index.stats.pitFallbacks += 1;
|
|
444
|
-
recordStat(index, bucket, reason);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function recordStat(index, bucket, reason) {
|
|
448
|
-
if (!index?.stats || !reason) return;
|
|
449
|
-
const target = index.stats[bucket] ?? {};
|
|
450
|
-
target[reason] = (target[reason] ?? 0) + 1;
|
|
451
|
-
index.stats[bucket] = target;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
388
|
function createTrackBands(track) {
|
|
455
389
|
const trackEdge = track.width / 2;
|
|
456
390
|
const kerbEdge = trackEdge + (track.kerbWidth ?? 0);
|
|
@@ -559,18 +493,6 @@ function expandedSampleBounds(samples, expansion) {
|
|
|
559
493
|
};
|
|
560
494
|
}
|
|
561
495
|
|
|
562
|
-
function createSpatialGrid(bounds, cellSize) {
|
|
563
|
-
const columns = Math.max(1, Math.ceil((bounds.maxX - bounds.minX) / cellSize));
|
|
564
|
-
const rows = Math.max(1, Math.ceil((bounds.maxY - bounds.minY) / cellSize));
|
|
565
|
-
return {
|
|
566
|
-
bounds,
|
|
567
|
-
cellSize,
|
|
568
|
-
columns,
|
|
569
|
-
rows,
|
|
570
|
-
cells: new Array(columns * rows),
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
|
|
574
496
|
function insertSegmentIntoGrid(grid, centerline, segmentId, expansion) {
|
|
575
497
|
insertIdIntoGridBounds(grid, segmentId, {
|
|
576
498
|
minX: centerline.minX[segmentId] - expansion,
|
|
@@ -580,19 +502,6 @@ function insertSegmentIntoGrid(grid, centerline, segmentId, expansion) {
|
|
|
580
502
|
});
|
|
581
503
|
}
|
|
582
504
|
|
|
583
|
-
function insertIdIntoGridBounds(grid, id, bounds) {
|
|
584
|
-
const minCell = gridCellForPoint(grid, { x: bounds.minX, y: bounds.minY }, true);
|
|
585
|
-
const maxCell = gridCellForPoint(grid, { x: bounds.maxX, y: bounds.maxY }, true);
|
|
586
|
-
for (let row = minCell.row; row <= maxCell.row; row += 1) {
|
|
587
|
-
for (let column = minCell.column; column <= maxCell.column; column += 1) {
|
|
588
|
-
const cellIndex = row * grid.columns + column;
|
|
589
|
-
const cell = grid.cells[cellIndex];
|
|
590
|
-
if (cell) cell.push(id);
|
|
591
|
-
else grid.cells[cellIndex] = [id];
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
505
|
function candidateIdsFromArcBuckets(index, distanceAlong, radius) {
|
|
597
506
|
const arcBuckets = index?.arcBuckets;
|
|
598
507
|
if (!arcBuckets?.count || !Number.isFinite(distanceAlong)) return [];
|
|
@@ -613,73 +522,6 @@ function candidateIdsFromArcBuckets(index, distanceAlong, radius) {
|
|
|
613
522
|
return ids;
|
|
614
523
|
}
|
|
615
524
|
|
|
616
|
-
function candidateIdsFromGrid(index, grid, position, neighborLimit) {
|
|
617
|
-
const center = gridCellForPoint(grid, position, false);
|
|
618
|
-
if (!center) return [];
|
|
619
|
-
const scratch = ensureQueryScratch(index);
|
|
620
|
-
const maxId = Math.max(index.centerline.segmentCount, index.pit?.roadSegments?.length ?? 0, index.pit?.boxCandidates?.length ?? 0);
|
|
621
|
-
const idMarks = ensureScratchArray(scratch, 'candidateMarks', maxId);
|
|
622
|
-
const ids = [];
|
|
623
|
-
for (let radius = 0; radius <= neighborLimit; radius += 1) {
|
|
624
|
-
const candidateEpoch = nextScratchEpoch(scratch, 'candidateEpoch', idMarks);
|
|
625
|
-
ids.length = 0;
|
|
626
|
-
for (let row = center.row - radius; row <= center.row + radius; row += 1) {
|
|
627
|
-
for (let column = center.column - radius; column <= center.column + radius; column += 1) {
|
|
628
|
-
if (column < 0 || row < 0 || column >= grid.columns || row >= grid.rows) continue;
|
|
629
|
-
const cell = grid.cells[row * grid.columns + column];
|
|
630
|
-
if (!cell) continue;
|
|
631
|
-
for (const id of cell) {
|
|
632
|
-
if (idMarks[id] === candidateEpoch) continue;
|
|
633
|
-
idMarks[id] = candidateEpoch;
|
|
634
|
-
ids.push(id);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
if (ids.length) return ids.slice();
|
|
639
|
-
}
|
|
640
|
-
return [];
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function candidateIdsFromGridBounds(index, grid, bounds) {
|
|
644
|
-
if (
|
|
645
|
-
bounds.maxX < grid.bounds.minX ||
|
|
646
|
-
bounds.minX > grid.bounds.maxX ||
|
|
647
|
-
bounds.maxY < grid.bounds.minY ||
|
|
648
|
-
bounds.minY > grid.bounds.maxY
|
|
649
|
-
) return [];
|
|
650
|
-
const minPoint = { x: bounds.minX, y: bounds.minY };
|
|
651
|
-
const maxPoint = { x: bounds.maxX, y: bounds.maxY };
|
|
652
|
-
const minCell = gridCellForPoint(grid, minPoint, true);
|
|
653
|
-
const maxCell = gridCellForPoint(grid, maxPoint, true);
|
|
654
|
-
const scratch = ensureQueryScratch(index);
|
|
655
|
-
const maxId = Math.max(index.centerline.segmentCount, index.pit?.roadSegments?.length ?? 0, index.pit?.boxCandidates?.length ?? 0);
|
|
656
|
-
const idMarks = ensureScratchArray(scratch, 'candidateMarks', maxId);
|
|
657
|
-
const candidateEpoch = nextScratchEpoch(scratch, 'candidateEpoch', idMarks);
|
|
658
|
-
const ids = [];
|
|
659
|
-
for (let row = minCell.row; row <= maxCell.row; row += 1) {
|
|
660
|
-
for (let column = minCell.column; column <= maxCell.column; column += 1) {
|
|
661
|
-
const cell = grid.cells[row * grid.columns + column];
|
|
662
|
-
if (!cell) continue;
|
|
663
|
-
for (const id of cell) {
|
|
664
|
-
if (idMarks[id] === candidateEpoch) continue;
|
|
665
|
-
idMarks[id] = candidateEpoch;
|
|
666
|
-
ids.push(id);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
return ids;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function gridCellForPoint(grid, point, clampToGrid) {
|
|
674
|
-
const column = Math.floor((point.x - grid.bounds.minX) / grid.cellSize);
|
|
675
|
-
const row = Math.floor((point.y - grid.bounds.minY) / grid.cellSize);
|
|
676
|
-
if (!clampToGrid && (column < 0 || row < 0 || column >= grid.columns || row >= grid.rows)) return null;
|
|
677
|
-
return {
|
|
678
|
-
column: clampToGrid ? clamp(column, 0, grid.columns - 1) : column,
|
|
679
|
-
row: clampToGrid ? clamp(row, 0, grid.rows - 1) : row,
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
|
|
683
525
|
function projectIndexedSegment(centerline, segmentId, position) {
|
|
684
526
|
const ax = centerline.startX[segmentId];
|
|
685
527
|
const ay = centerline.startY[segmentId];
|
|
@@ -733,134 +575,6 @@ function segmentFromIndex(centerline, segmentId) {
|
|
|
733
575
|
};
|
|
734
576
|
}
|
|
735
577
|
|
|
736
|
-
function createPitQueryIndex(track, cellSize) {
|
|
737
|
-
const pitLane = track.pitLane;
|
|
738
|
-
if (!pitLane?.enabled) return null;
|
|
739
|
-
const bounds = {
|
|
740
|
-
minX: pitLane.bounds.minX - cellSize,
|
|
741
|
-
maxX: pitLane.bounds.maxX + cellSize,
|
|
742
|
-
minY: pitLane.bounds.minY - cellSize,
|
|
743
|
-
maxY: pitLane.bounds.maxY + cellSize,
|
|
744
|
-
};
|
|
745
|
-
const roadGrid = createSpatialGrid(bounds, cellSize);
|
|
746
|
-
const boxGrid = createSpatialGrid(bounds, cellSize);
|
|
747
|
-
const roadSegments = [];
|
|
748
|
-
const routes = {};
|
|
749
|
-
|
|
750
|
-
[
|
|
751
|
-
['entry', pitLane.entry?.roadCenterline],
|
|
752
|
-
['main', pitLane.mainLane?.points],
|
|
753
|
-
['working', pitLane.workingLane?.points],
|
|
754
|
-
['exit', pitLane.exit?.roadCenterline],
|
|
755
|
-
].forEach(([routeId, points]) => {
|
|
756
|
-
routes[routeId] = { start: roadSegments.length };
|
|
757
|
-
indexPitRoute(roadGrid, roadSegments, routeId, points, cellSize);
|
|
758
|
-
routes[routeId].end = roadSegments.length;
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
const boxCandidates = [];
|
|
762
|
-
[
|
|
763
|
-
...(pitLane.serviceAreas ?? []).flatMap((area) => [
|
|
764
|
-
{ type: 'service-area', target: area, polygon: area.corners },
|
|
765
|
-
{ type: 'service-queue', target: area, polygon: area.queueCorners },
|
|
766
|
-
]),
|
|
767
|
-
...(pitLane.boxes ?? []).map((box) => ({ type: 'garage-box', target: box, polygon: box.corners })),
|
|
768
|
-
]
|
|
769
|
-
.filter((candidate) => Array.isArray(candidate.polygon) && candidate.polygon.length >= 3)
|
|
770
|
-
.forEach((candidate) => {
|
|
771
|
-
const id = boxCandidates.length;
|
|
772
|
-
boxCandidates.push(candidate);
|
|
773
|
-
insertIdIntoGridBounds(boxGrid, id, polygonBounds(candidate.polygon, cellSize));
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
return {
|
|
777
|
-
roadGrid,
|
|
778
|
-
boxGrid,
|
|
779
|
-
routes,
|
|
780
|
-
roadSegments,
|
|
781
|
-
boxCandidates,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function indexPitRoute(grid, roadSegments, routeId, points, expansion) {
|
|
786
|
-
if (!Array.isArray(points) || points.length < 2) return;
|
|
787
|
-
for (let index = 0; index < points.length - 1; index += 1) {
|
|
788
|
-
const start = points[index];
|
|
789
|
-
const end = points[index + 1];
|
|
790
|
-
const id = roadSegments.length;
|
|
791
|
-
roadSegments.push({ routeId, segmentIndex: index });
|
|
792
|
-
const bounds = {
|
|
793
|
-
minX: Math.min(start.x, end.x) - expansion,
|
|
794
|
-
maxX: Math.max(start.x, end.x) + expansion,
|
|
795
|
-
minY: Math.min(start.y, end.y) - expansion,
|
|
796
|
-
maxY: Math.max(start.y, end.y) + expansion,
|
|
797
|
-
};
|
|
798
|
-
insertIdIntoGridBounds(grid, id, bounds);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function polygonBounds(polygon, expansion) {
|
|
803
|
-
const bounds = polygon.reduce((current, point) => ({
|
|
804
|
-
minX: Math.min(current.minX, point.x),
|
|
805
|
-
maxX: Math.max(current.maxX, point.x),
|
|
806
|
-
minY: Math.min(current.minY, point.y),
|
|
807
|
-
maxY: Math.max(current.maxY, point.y),
|
|
808
|
-
}), { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity });
|
|
809
|
-
return {
|
|
810
|
-
minX: bounds.minX - expansion,
|
|
811
|
-
maxX: bounds.maxX + expansion,
|
|
812
|
-
minY: bounds.minY - expansion,
|
|
813
|
-
maxY: bounds.maxY + expansion,
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function createQueryScratch(index) {
|
|
818
|
-
const segmentCount = Math.max(0, index?.centerline?.segmentCount ?? 0);
|
|
819
|
-
const maxId = Math.max(
|
|
820
|
-
segmentCount,
|
|
821
|
-
index?.pit?.roadSegments?.length ?? 0,
|
|
822
|
-
index?.pit?.boxCandidates?.length ?? 0,
|
|
823
|
-
);
|
|
824
|
-
const grid = index?.segmentGrid ?? index?.grid;
|
|
825
|
-
const cellCount = Math.max(0, (grid?.columns ?? 0) * (grid?.rows ?? 0));
|
|
826
|
-
return {
|
|
827
|
-
candidateMarks: new Uint32Array(Math.max(1, maxId)),
|
|
828
|
-
candidateEpoch: 1,
|
|
829
|
-
sampleMarks: new Uint32Array(Math.max(1, segmentCount)),
|
|
830
|
-
sampleEpoch: 1,
|
|
831
|
-
raySegmentMarks: new Uint32Array(Math.max(1, segmentCount)),
|
|
832
|
-
raySegmentEpoch: 1,
|
|
833
|
-
rayCellMarks: new Uint32Array(Math.max(1, cellCount)),
|
|
834
|
-
rayCellEpoch: 1,
|
|
835
|
-
raySegmentIds: [],
|
|
836
|
-
rayVisitedCells: [],
|
|
837
|
-
rayTraceCache: new Map(),
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
function ensureQueryScratch(index) {
|
|
842
|
-
if (index.queryScratch) return index.queryScratch;
|
|
843
|
-
index.queryScratch = createQueryScratch(index);
|
|
844
|
-
return index.queryScratch;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function ensureScratchArray(scratch, key, minimumLength) {
|
|
848
|
-
if (!scratch[key] || scratch[key].length < minimumLength) {
|
|
849
|
-
scratch[key] = new Uint32Array(Math.max(1, minimumLength));
|
|
850
|
-
}
|
|
851
|
-
return scratch[key];
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function nextScratchEpoch(scratch, key, marks) {
|
|
855
|
-
let epoch = (scratch[key] ?? 0) + 1;
|
|
856
|
-
if (epoch >= 0xFFFFFFFF) {
|
|
857
|
-
marks.fill(0);
|
|
858
|
-
epoch = 1;
|
|
859
|
-
}
|
|
860
|
-
scratch[key] = epoch;
|
|
861
|
-
return epoch;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
578
|
function seedRayTraceFromNearbyCache(
|
|
865
579
|
index,
|
|
866
580
|
grid,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createSpatialGrid, insertIdIntoGridBounds, candidateIdsFromGrid } from './trackQueryGrid.js';
|
|
2
|
+
import { recordFallback, recordStat } from './trackQueryStats.js';
|
|
3
|
+
|
|
4
|
+
export function createPitQueryIndex(track, cellSize) {
|
|
5
|
+
const pitLane = track.pitLane;
|
|
6
|
+
if (!pitLane?.enabled) return null;
|
|
7
|
+
const bounds = {
|
|
8
|
+
minX: pitLane.bounds.minX - cellSize,
|
|
9
|
+
maxX: pitLane.bounds.maxX + cellSize,
|
|
10
|
+
minY: pitLane.bounds.minY - cellSize,
|
|
11
|
+
maxY: pitLane.bounds.maxY + cellSize,
|
|
12
|
+
};
|
|
13
|
+
const roadGrid = createSpatialGrid(bounds, cellSize);
|
|
14
|
+
const boxGrid = createSpatialGrid(bounds, cellSize);
|
|
15
|
+
const roadSegments = [];
|
|
16
|
+
const routes = {};
|
|
17
|
+
|
|
18
|
+
[
|
|
19
|
+
['entry', pitLane.entry?.roadCenterline],
|
|
20
|
+
['main', pitLane.mainLane?.points],
|
|
21
|
+
['working', pitLane.workingLane?.points],
|
|
22
|
+
['exit', pitLane.exit?.roadCenterline],
|
|
23
|
+
].forEach(([routeId, points]) => {
|
|
24
|
+
routes[routeId] = { start: roadSegments.length };
|
|
25
|
+
indexPitRoute(roadGrid, roadSegments, routeId, points, cellSize);
|
|
26
|
+
routes[routeId].end = roadSegments.length;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const boxCandidates = [];
|
|
30
|
+
[
|
|
31
|
+
...(pitLane.serviceAreas ?? []).flatMap((area) => [
|
|
32
|
+
{ type: 'service-area', target: area, polygon: area.corners },
|
|
33
|
+
{ type: 'service-queue', target: area, polygon: area.queueCorners },
|
|
34
|
+
]),
|
|
35
|
+
...(pitLane.boxes ?? []).map((box) => ({ type: 'garage-box', target: box, polygon: box.corners })),
|
|
36
|
+
]
|
|
37
|
+
.filter((candidate) => Array.isArray(candidate.polygon) && candidate.polygon.length >= 3)
|
|
38
|
+
.forEach((candidate) => {
|
|
39
|
+
const id = boxCandidates.length;
|
|
40
|
+
boxCandidates.push(candidate);
|
|
41
|
+
insertIdIntoGridBounds(boxGrid, id, polygonBounds(candidate.polygon, cellSize));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
roadGrid,
|
|
46
|
+
boxGrid,
|
|
47
|
+
routes,
|
|
48
|
+
roadSegments,
|
|
49
|
+
boxCandidates,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function queryPitBoxCandidates(track, position) {
|
|
54
|
+
const index = track?.queryIndex;
|
|
55
|
+
const pit = index?.pit;
|
|
56
|
+
if (!pit?.boxGrid) {
|
|
57
|
+
recordFallback(index, 'pitFallbackReasons', 'missing-box-index');
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (!finitePoint(position)) {
|
|
61
|
+
recordFallback(index, 'pitFallbackReasons', 'invalid-position');
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
index.stats.pitQueries += 1;
|
|
65
|
+
const ids = candidateIdsFromGrid(index, pit.boxGrid, position, 1);
|
|
66
|
+
if (!ids.length) {
|
|
67
|
+
recordStat(index, 'pitPaths', 'box-grid-miss');
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
recordStat(index, 'pitPaths', 'box-grid-hit');
|
|
71
|
+
return ids.map((id) => pit.boxCandidates[id]).filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function queryPitRoadSegmentCandidates(track, routeId, position) {
|
|
75
|
+
const candidatesByRoute = queryPitRoadSegmentCandidatesByRoute(track, position);
|
|
76
|
+
return candidatesByRoute?.[routeId] ?? candidatesByRoute;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function queryPitRoadSegmentCandidatesByRoute(track, position) {
|
|
80
|
+
const index = track?.queryIndex;
|
|
81
|
+
const pit = index?.pit;
|
|
82
|
+
if (!pit?.roadGrid) {
|
|
83
|
+
recordFallback(index, 'pitFallbackReasons', 'missing-road-index');
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (!finitePoint(position)) {
|
|
87
|
+
recordFallback(index, 'pitFallbackReasons', 'invalid-position');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
index.stats.pitQueries += 1;
|
|
91
|
+
const ids = candidateIdsFromGrid(index, pit.roadGrid, position, 1);
|
|
92
|
+
if (!ids.length) {
|
|
93
|
+
recordStat(index, 'pitPaths', 'road-grid-miss');
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const candidatesByRoute = { entry: [], main: [], working: [], exit: [] };
|
|
97
|
+
ids.forEach((id) => {
|
|
98
|
+
const segment = pit.roadSegments[id];
|
|
99
|
+
if (!segment || !candidatesByRoute[segment.routeId]) return;
|
|
100
|
+
candidatesByRoute[segment.routeId].push(segment.segmentIndex);
|
|
101
|
+
});
|
|
102
|
+
recordStat(index, 'pitPaths', 'road-grid-hit');
|
|
103
|
+
return candidatesByRoute;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function indexPitRoute(grid, roadSegments, routeId, points, expansion) {
|
|
107
|
+
if (!Array.isArray(points) || points.length < 2) return;
|
|
108
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
109
|
+
const start = points[index];
|
|
110
|
+
const end = points[index + 1];
|
|
111
|
+
const id = roadSegments.length;
|
|
112
|
+
roadSegments.push({ routeId, segmentIndex: index });
|
|
113
|
+
const bounds = {
|
|
114
|
+
minX: Math.min(start.x, end.x) - expansion,
|
|
115
|
+
maxX: Math.max(start.x, end.x) + expansion,
|
|
116
|
+
minY: Math.min(start.y, end.y) - expansion,
|
|
117
|
+
maxY: Math.max(start.y, end.y) + expansion,
|
|
118
|
+
};
|
|
119
|
+
insertIdIntoGridBounds(grid, id, bounds);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function polygonBounds(polygon, expansion) {
|
|
124
|
+
const bounds = polygon.reduce((current, point) => ({
|
|
125
|
+
minX: Math.min(current.minX, point.x),
|
|
126
|
+
maxX: Math.max(current.maxX, point.x),
|
|
127
|
+
minY: Math.min(current.minY, point.y),
|
|
128
|
+
maxY: Math.max(current.maxY, point.y),
|
|
129
|
+
}), { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity });
|
|
130
|
+
return {
|
|
131
|
+
minX: bounds.minX - expansion,
|
|
132
|
+
maxX: bounds.maxX + expansion,
|
|
133
|
+
minY: bounds.minY - expansion,
|
|
134
|
+
maxY: bounds.maxY + expansion,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function finitePoint(point) {
|
|
139
|
+
return Number.isFinite(point?.x) && Number.isFinite(point?.y);
|
|
140
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function createQueryScratch(index) {
|
|
2
|
+
const segmentCount = Math.max(0, index?.centerline?.segmentCount ?? 0);
|
|
3
|
+
const maxId = Math.max(
|
|
4
|
+
segmentCount,
|
|
5
|
+
index?.pit?.roadSegments?.length ?? 0,
|
|
6
|
+
index?.pit?.boxCandidates?.length ?? 0,
|
|
7
|
+
);
|
|
8
|
+
const grid = index?.segmentGrid ?? index?.grid;
|
|
9
|
+
const cellCount = Math.max(0, (grid?.columns ?? 0) * (grid?.rows ?? 0));
|
|
10
|
+
return {
|
|
11
|
+
candidateMarks: new Uint32Array(Math.max(1, maxId)),
|
|
12
|
+
candidateEpoch: 1,
|
|
13
|
+
sampleMarks: new Uint32Array(Math.max(1, segmentCount)),
|
|
14
|
+
sampleEpoch: 1,
|
|
15
|
+
raySegmentMarks: new Uint32Array(Math.max(1, segmentCount)),
|
|
16
|
+
raySegmentEpoch: 1,
|
|
17
|
+
rayCellMarks: new Uint32Array(Math.max(1, cellCount)),
|
|
18
|
+
rayCellEpoch: 1,
|
|
19
|
+
raySegmentIds: [],
|
|
20
|
+
rayVisitedCells: [],
|
|
21
|
+
rayTraceCache: new Map(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ensureQueryScratch(index) {
|
|
26
|
+
if (index.queryScratch) return index.queryScratch;
|
|
27
|
+
index.queryScratch = createQueryScratch(index);
|
|
28
|
+
return index.queryScratch;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ensureScratchArray(scratch, key, minimumLength) {
|
|
32
|
+
if (!scratch[key] || scratch[key].length < minimumLength) {
|
|
33
|
+
scratch[key] = new Uint32Array(Math.max(1, minimumLength));
|
|
34
|
+
}
|
|
35
|
+
return scratch[key];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function nextScratchEpoch(scratch, key, marks) {
|
|
39
|
+
let epoch = (scratch[key] ?? 0) + 1;
|
|
40
|
+
if (epoch >= 0xFFFFFFFF) {
|
|
41
|
+
marks.fill(0);
|
|
42
|
+
epoch = 1;
|
|
43
|
+
}
|
|
44
|
+
scratch[key] = epoch;
|
|
45
|
+
return epoch;
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function createQueryStats() {
|
|
2
|
+
return {
|
|
3
|
+
nearestQueries: 0,
|
|
4
|
+
nearestFallbacks: 0,
|
|
5
|
+
nearestPaths: {},
|
|
6
|
+
nearestFallbackReasons: {
|
|
7
|
+
'spatial-grid-no-candidates': 0,
|
|
8
|
+
},
|
|
9
|
+
pitQueries: 0,
|
|
10
|
+
pitFallbacks: 0,
|
|
11
|
+
pitPaths: {},
|
|
12
|
+
pitFallbackReasons: {},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function recordFallback(index, bucket, reason) {
|
|
17
|
+
if (!index?.stats) return;
|
|
18
|
+
if (bucket === 'nearestFallbackReasons') index.stats.nearestFallbacks += 1;
|
|
19
|
+
if (bucket === 'pitFallbackReasons') index.stats.pitFallbacks += 1;
|
|
20
|
+
recordStat(index, bucket, reason);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function recordStat(index, bucket, key) {
|
|
24
|
+
if (!index?.stats || !bucket || !key) return;
|
|
25
|
+
const target = index.stats[bucket] ?? {};
|
|
26
|
+
target[key] = (target[key] ?? 0) + 1;
|
|
27
|
+
index.stats[bucket] = target;
|
|
28
|
+
}
|
|
@@ -15,10 +15,32 @@ function forwardVector(car) {
|
|
|
15
15
|
return { x: Math.cos(car.heading), y: Math.sin(car.heading) };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function velocityVector(car) {
|
|
19
|
+
if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
|
|
20
|
+
return { x: car.velocityX, y: car.velocityY };
|
|
21
|
+
}
|
|
22
|
+
const forward = forwardVector(car);
|
|
23
|
+
return { x: forward.x * car.speed, y: forward.y * car.speed };
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
function dot(a, b) {
|
|
19
27
|
return a.x * b.x + a.y * b.y;
|
|
20
28
|
}
|
|
21
29
|
|
|
30
|
+
function applyVelocityVector(car, velocity) {
|
|
31
|
+
const speed = clamp(Math.hypot(velocity.x, velocity.y), 0, VEHICLE_LIMITS.maxSpeed);
|
|
32
|
+
if (speed <= 0) {
|
|
33
|
+
car.velocityX = 0;
|
|
34
|
+
car.velocityY = 0;
|
|
35
|
+
car.speed = 0;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const scale = speed / Math.max(1e-9, Math.hypot(velocity.x, velocity.y));
|
|
39
|
+
car.velocityX = velocity.x * scale;
|
|
40
|
+
car.velocityY = velocity.y * scale;
|
|
41
|
+
car.speed = speed;
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
export function resolveCollisionsForSimulation(sim) {
|
|
23
45
|
const collidableCars = sim.cars.filter(isCollidable);
|
|
24
46
|
if (collidableCars.length < 2) return;
|
|
@@ -57,8 +79,8 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
57
79
|
}
|
|
58
80
|
|
|
59
81
|
if (oneCarFixed) {
|
|
60
|
-
if (!firstPitControlled)
|
|
61
|
-
if (!secondPitControlled)
|
|
82
|
+
if (!firstPitControlled) dampPitContactVelocity(first, 0.985, sim.physicsMode === 'simulator');
|
|
83
|
+
if (!secondPitControlled) dampPitContactVelocity(second, 0.985, sim.physicsMode === 'simulator');
|
|
62
84
|
} else {
|
|
63
85
|
applyContactVelocityResponse(sim, first, second, collision.axis);
|
|
64
86
|
}
|
|
@@ -99,6 +121,37 @@ export function resolveCollisionsForSimulation(sim) {
|
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
export function applyContactVelocityResponse(sim, first, second, axis) {
|
|
124
|
+
if (
|
|
125
|
+
sim.physicsMode === 'simulator' ||
|
|
126
|
+
Number.isFinite(first.velocityX) ||
|
|
127
|
+
Number.isFinite(second.velocityX)
|
|
128
|
+
) {
|
|
129
|
+
const firstVelocity = velocityVector(first);
|
|
130
|
+
const secondVelocity = velocityVector(second);
|
|
131
|
+
const relativeNormalVelocity = dot({
|
|
132
|
+
x: secondVelocity.x - firstVelocity.x,
|
|
133
|
+
y: secondVelocity.y - firstVelocity.y,
|
|
134
|
+
}, axis);
|
|
135
|
+
|
|
136
|
+
if (relativeNormalVelocity < 0) {
|
|
137
|
+
const impulse = clamp(-relativeNormalVelocity * (0.34 + sim.rules.collisionRestitution), 0, 16);
|
|
138
|
+
firstVelocity.x -= axis.x * impulse;
|
|
139
|
+
firstVelocity.y -= axis.y * impulse;
|
|
140
|
+
secondVelocity.x += axis.x * impulse;
|
|
141
|
+
secondVelocity.y += axis.y * impulse;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
applyVelocityVector(first, {
|
|
145
|
+
x: firstVelocity.x * 0.997,
|
|
146
|
+
y: firstVelocity.y * 0.997,
|
|
147
|
+
});
|
|
148
|
+
applyVelocityVector(second, {
|
|
149
|
+
x: secondVelocity.x * 0.997,
|
|
150
|
+
y: secondVelocity.y * 0.997,
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
102
155
|
const firstForward = forwardVector(first);
|
|
103
156
|
const secondForward = forwardVector(second);
|
|
104
157
|
const firstNormal = dot(firstForward, axis);
|
|
@@ -114,3 +167,15 @@ export function applyContactVelocityResponse(sim, first, second, axis) {
|
|
|
114
167
|
first.speed = clamp(first.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
115
168
|
second.speed = clamp(second.speed * 0.997, 0, VEHICLE_LIMITS.maxSpeed);
|
|
116
169
|
}
|
|
170
|
+
|
|
171
|
+
function dampPitContactVelocity(car, factor, syncVelocity = false) {
|
|
172
|
+
if (syncVelocity || Number.isFinite(car.velocityX) || Number.isFinite(car.velocityY)) {
|
|
173
|
+
const velocity = velocityVector(car);
|
|
174
|
+
applyVelocityVector(car, {
|
|
175
|
+
x: velocity.x * factor,
|
|
176
|
+
y: velocity.y * factor,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
car.speed = clamp(car.speed * factor, 0, VEHICLE_LIMITS.maxSpeed);
|
|
181
|
+
}
|