@inventure71/paddockjs 2.0.0 → 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 +37 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +92 -17
- package/docs/architecture.md +26 -16
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +7 -5
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +121 -33
- package/docs/data_model.md +1 -1
- package/docs/index.md +10 -0
- package/docs/learnings.md +6 -6
- package/docs/rules.md +39 -9
- package/docs/sense_contract.md +59 -0
- package/docs/system_specs.md +76 -20
- package/docs/training.md +27 -20
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +75 -36
- package/src/app/BrowserExpertAdapter.js +9 -300
- package/src/app/BrowserExpertExternalRenderer.js +303 -0
- package/src/app/F1SimulatorApp.js +190 -68
- 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/restartOptions.js +34 -0
- 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 +34 -5
- package/src/environment/episode.js +14 -2
- package/src/environment/index.d.ts +17 -4
- package/src/environment/observationVector.js +200 -0
- package/src/environment/observations.js +58 -285
- package/src/environment/options.js +1 -1
- package/src/environment/recorder.js +13 -6
- package/src/environment/rewards.js +13 -2
- package/src/environment/runtime.js +65 -25
- 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 +84 -12
- package/src/environment/sensors/trackRays.js +173 -15
- package/src/environment/specs.js +78 -40
- package/src/index.d.ts +112 -9
- package/src/index.js +26 -33
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- package/src/simulation/pit/pitFlow.js +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/pitRouting.js +2 -4
- 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/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +2 -2
- 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/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/trackQueryGrid.js +94 -0
- package/src/simulation/track/trackQueryIndex.js +193 -434
- 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/track/trackStatePolicy.js +0 -1
- 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 +5 -4
- package/src/simulation/vehicle/vehicleSnapshots.js +22 -0
- package/src/simulation/vehicle/vehicleState.js +36 -2
- package/src/simulation/vehicle/wheelSurface.js +3 -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
|
@@ -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;
|
|
@@ -27,6 +43,7 @@ export function createTrackQueryIndex(track) {
|
|
|
27
43
|
const index = {
|
|
28
44
|
version: 1,
|
|
29
45
|
bands,
|
|
46
|
+
nearestGridExpansion: expansion,
|
|
30
47
|
centerline,
|
|
31
48
|
grid,
|
|
32
49
|
segmentGrid,
|
|
@@ -38,7 +55,7 @@ export function createTrackQueryIndex(track) {
|
|
|
38
55
|
return index;
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
export function queryNearestTrackProjection(track, position, progressHint = null
|
|
58
|
+
export function queryNearestTrackProjection(track, position, progressHint = null) {
|
|
42
59
|
const index = track?.queryIndex;
|
|
43
60
|
if (!index?.centerline?.segmentCount) {
|
|
44
61
|
recordFallback(index, 'nearestFallbackReasons', 'missing-index');
|
|
@@ -50,8 +67,7 @@ export function queryNearestTrackProjection(track, position, progressHint = null
|
|
|
50
67
|
}
|
|
51
68
|
index.stats.nearestQueries += 1;
|
|
52
69
|
|
|
53
|
-
const
|
|
54
|
-
const { projection, path, reason } = bestNearestFromIndex(track, index, position, progressHint, mode, options);
|
|
70
|
+
const { projection, path, reason } = bestProjectionFromIndex(index, position, progressHint);
|
|
55
71
|
if (!projection) {
|
|
56
72
|
recordFallback(index, 'nearestFallbackReasons', reason ?? 'unknown');
|
|
57
73
|
return null;
|
|
@@ -61,169 +77,173 @@ export function queryNearestTrackProjection(track, position, progressHint = null
|
|
|
61
77
|
return projection;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
function
|
|
65
|
-
return mode === 'sample'
|
|
66
|
-
? bestSampleFromIndex(track, index, position, progressHint, options)
|
|
67
|
-
: bestProjectionFromIndex(index, position, progressHint, options);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function bestProjectionFromIndex(index, position, progressHint, options) {
|
|
80
|
+
function bestProjectionFromIndex(index, position, progressHint) {
|
|
71
81
|
const hintedCandidateIds = Number.isFinite(progressHint)
|
|
72
82
|
? candidateIdsFromArcBuckets(index, progressHint, 2)
|
|
73
83
|
: [];
|
|
74
84
|
if (hintedCandidateIds.length) {
|
|
75
85
|
const hinted = bestProjectionFromCandidates(index.centerline, hintedCandidateIds, position, progressHint);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
}
|
|
82
97
|
}
|
|
83
|
-
recordStat(index, 'nearestPaths', hinted.projection ? 'arc-hint-rejected-distance' : `arc-hint-${hinted.reason}`);
|
|
84
98
|
}
|
|
85
99
|
|
|
100
|
+
let local = { projection: null, path: null };
|
|
86
101
|
const gridCandidateIds = candidateIdsFromGrid(index, index.grid, position, GRID_NEIGHBOR_LIMIT);
|
|
87
102
|
const grid = bestProjectionFromCandidates(index.centerline, gridCandidateIds, position, progressHint);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
if (grid.projection) {
|
|
104
|
+
local = chooseBetterProjectionResult(local, grid, 'spatial-grid', progressHint);
|
|
105
|
+
if (grid.projection.distanceSquared <= index.nearestGridExpansion * index.nearestGridExpansion) {
|
|
106
|
+
return local;
|
|
92
107
|
}
|
|
93
|
-
|
|
94
|
-
}
|
|
108
|
+
}
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
: [];
|
|
103
|
-
if (hintedCandidateIds.length) {
|
|
104
|
-
const hinted = bestSampleFromCandidates(index, samples, sampleCount, hintedCandidateIds, position, progressHint);
|
|
105
|
-
const maxHintDistance = resolveHintMaxDistance(index, options);
|
|
106
|
-
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`;
|
|
107
116
|
return {
|
|
108
|
-
projection:
|
|
109
|
-
path:
|
|
117
|
+
projection: exact.projection,
|
|
118
|
+
path: exact.reason === 'tie-resolved' && !path.endsWith('tie-resolved') ? `${path}-tie-resolved` : path,
|
|
110
119
|
};
|
|
111
120
|
}
|
|
112
|
-
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
projection: local.projection,
|
|
124
|
+
path: local.path,
|
|
125
|
+
};
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
return
|
|
128
|
+
const exact = bestProjectionFromCellSearch(index, position, progressHint);
|
|
129
|
+
const global = exact.projection ? exact : bestProjectionFromAllSegments(index.centerline, position, progressHint);
|
|
130
|
+
return global.projection
|
|
118
131
|
? {
|
|
119
|
-
projection:
|
|
120
|
-
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'),
|
|
121
136
|
}
|
|
122
|
-
: { projection: null, reason: `
|
|
137
|
+
: { projection: null, reason: `global-index-${global.reason}` };
|
|
123
138
|
}
|
|
124
139
|
|
|
125
|
-
function
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
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;
|
|
129
155
|
}
|
|
130
156
|
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const sampleMarks = ensureScratchArray(scratch, 'sampleMarks', sampleCount);
|
|
135
|
-
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);
|
|
136
160
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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;
|
|
140
169
|
let secondBestDistance = Infinity;
|
|
141
170
|
let tieResolved = false;
|
|
142
|
-
for (const segmentId of candidateSegmentIds) {
|
|
143
|
-
const normalized = ((segmentId % sampleCount) + sampleCount) % sampleCount;
|
|
144
|
-
const adjacent = (normalized + 1) % sampleCount;
|
|
145
|
-
if (sampleMarks[normalized] !== sampleEpoch) {
|
|
146
|
-
sampleMarks[normalized] = sampleEpoch;
|
|
147
|
-
({ bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved } = considerSampleCandidate({
|
|
148
|
-
samples,
|
|
149
|
-
sampleId: normalized,
|
|
150
|
-
position,
|
|
151
|
-
preferredDistance,
|
|
152
|
-
bestSample,
|
|
153
|
-
bestDistanceSquared,
|
|
154
|
-
bestTieScore,
|
|
155
|
-
secondBestDistance,
|
|
156
|
-
tieResolved,
|
|
157
|
-
}));
|
|
158
|
-
}
|
|
159
|
-
if (sampleMarks[adjacent] !== sampleEpoch) {
|
|
160
|
-
sampleMarks[adjacent] = sampleEpoch;
|
|
161
|
-
({ bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved } = considerSampleCandidate({
|
|
162
|
-
samples,
|
|
163
|
-
sampleId: adjacent,
|
|
164
|
-
position,
|
|
165
|
-
preferredDistance,
|
|
166
|
-
bestSample,
|
|
167
|
-
bestDistanceSquared,
|
|
168
|
-
bestTieScore,
|
|
169
|
-
secondBestDistance,
|
|
170
|
-
tieResolved,
|
|
171
|
-
}));
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
171
|
|
|
175
|
-
|
|
172
|
+
for (let radius = 0; radius <= maxRadius; radius += 1) {
|
|
173
|
+
let ringCanImprove = !best;
|
|
176
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
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!best) return { projection: null, reason: 'no-candidates' };
|
|
177
210
|
const ambiguous = Number.isFinite(secondBestDistance) &&
|
|
178
|
-
Math.abs(secondBestDistance -
|
|
179
|
-
return {
|
|
180
|
-
sample: bestSample,
|
|
181
|
-
distanceSquared: bestDistanceSquared,
|
|
182
|
-
reason: ambiguous || tieResolved ? 'tie-resolved' : 'ok',
|
|
183
|
-
};
|
|
211
|
+
Math.abs(secondBestDistance - best.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON;
|
|
212
|
+
return { projection: best, reason: ambiguous || tieResolved ? 'tie-resolved' : 'ok' };
|
|
184
213
|
}
|
|
185
214
|
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
secondBestDistance = bestDistanceSquared;
|
|
207
|
-
bestSample = sample;
|
|
208
|
-
bestDistanceSquared = distanceSquared;
|
|
209
|
-
bestTieScore = tieScore;
|
|
210
|
-
} else if (Math.abs(distanceSquared - bestDistanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON) {
|
|
211
|
-
tieResolved = true;
|
|
212
|
-
secondBestDistance = Math.min(secondBestDistance, distanceSquared);
|
|
213
|
-
if (tieScore < bestTieScore) {
|
|
214
|
-
bestSample = sample;
|
|
215
|
-
bestDistanceSquared = distanceSquared;
|
|
216
|
-
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);
|
|
217
235
|
}
|
|
218
|
-
} else if (distanceSquared < secondBestDistance) {
|
|
219
|
-
secondBestDistance = distanceSquared;
|
|
220
236
|
}
|
|
221
|
-
return { bestSample, bestDistanceSquared, bestTieScore, secondBestDistance, tieResolved };
|
|
222
237
|
}
|
|
223
238
|
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
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;
|
|
227
247
|
}
|
|
228
248
|
|
|
229
249
|
function bestProjectionFromCandidates(centerline, candidateIds, position, preferredDistance = null) {
|
|
@@ -242,7 +262,7 @@ function bestProjectionFromCandidates(centerline, candidateIds, position, prefer
|
|
|
242
262
|
} else if (Math.abs(projection.distanceSquared - best.distanceSquared) <= AMBIGUOUS_DISTANCE_EPSILON) {
|
|
243
263
|
tieResolved = true;
|
|
244
264
|
secondBestDistance = Math.min(secondBestDistance, projection.distanceSquared);
|
|
245
|
-
if (tieScore
|
|
265
|
+
if (projectionTieBeats(projection, tieScore, best, bestTieScore)) {
|
|
246
266
|
best = projection;
|
|
247
267
|
bestTieScore = tieScore;
|
|
248
268
|
}
|
|
@@ -258,62 +278,48 @@ function bestProjectionFromCandidates(centerline, candidateIds, position, prefer
|
|
|
258
278
|
return { projection: best, reason: ambiguous || tieResolved ? 'tie-resolved' : 'ok' };
|
|
259
279
|
}
|
|
260
280
|
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
return
|
|
264
|
-
|
|
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;
|
|
265
288
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
}
|
|
282
306
|
}
|
|
283
|
-
|
|
284
|
-
|
|
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' };
|
|
285
312
|
}
|
|
286
313
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return
|
|
314
|
+
function projectionTieScore(projection, preferredDistance) {
|
|
315
|
+
if (!Number.isFinite(preferredDistance)) return projection.segmentId;
|
|
316
|
+
return Math.abs(projection.distance - preferredDistance);
|
|
290
317
|
}
|
|
291
318
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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;
|
|
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;
|
|
317
323
|
}
|
|
318
324
|
|
|
319
325
|
export function queryNearbyTrackProjections(track, position, { neighborLimit = GRID_NEIGHBOR_LIMIT } = {}) {
|
|
@@ -401,13 +407,15 @@ export function attachTrackQueryIndex(track, queryIndex) {
|
|
|
401
407
|
return track;
|
|
402
408
|
}
|
|
403
409
|
|
|
404
|
-
export function forkTrackQueryIndex(sourceIndex) {
|
|
410
|
+
export function forkTrackQueryIndex(sourceIndex, runtimeTrack = null) {
|
|
405
411
|
if (!sourceIndex || typeof sourceIndex !== 'object') return null;
|
|
406
|
-
|
|
412
|
+
const index = {
|
|
407
413
|
...sourceIndex,
|
|
414
|
+
pit: runtimeTrack ? createPitQueryIndex(runtimeTrack, sourceIndex.grid?.cellSize ?? DEFAULT_GRID_CELL_SIZE) : sourceIndex.pit,
|
|
408
415
|
stats: createQueryStats(),
|
|
409
|
-
queryScratch: createQueryScratch(sourceIndex),
|
|
410
416
|
};
|
|
417
|
+
index.queryScratch = createQueryScratch(index);
|
|
418
|
+
return index;
|
|
411
419
|
}
|
|
412
420
|
|
|
413
421
|
export function resetTrackQueryStats(track) {
|
|
@@ -422,35 +430,6 @@ export function snapshotTrackQueryStats(track) {
|
|
|
422
430
|
return stats ? JSON.parse(JSON.stringify(stats)) : null;
|
|
423
431
|
}
|
|
424
432
|
|
|
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
433
|
function createTrackBands(track) {
|
|
455
434
|
const trackEdge = track.width / 2;
|
|
456
435
|
const kerbEdge = trackEdge + (track.kerbWidth ?? 0);
|
|
@@ -559,18 +538,6 @@ function expandedSampleBounds(samples, expansion) {
|
|
|
559
538
|
};
|
|
560
539
|
}
|
|
561
540
|
|
|
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
541
|
function insertSegmentIntoGrid(grid, centerline, segmentId, expansion) {
|
|
575
542
|
insertIdIntoGridBounds(grid, segmentId, {
|
|
576
543
|
minX: centerline.minX[segmentId] - expansion,
|
|
@@ -580,19 +547,6 @@ function insertSegmentIntoGrid(grid, centerline, segmentId, expansion) {
|
|
|
580
547
|
});
|
|
581
548
|
}
|
|
582
549
|
|
|
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
550
|
function candidateIdsFromArcBuckets(index, distanceAlong, radius) {
|
|
597
551
|
const arcBuckets = index?.arcBuckets;
|
|
598
552
|
if (!arcBuckets?.count || !Number.isFinite(distanceAlong)) return [];
|
|
@@ -613,73 +567,6 @@ function candidateIdsFromArcBuckets(index, distanceAlong, radius) {
|
|
|
613
567
|
return ids;
|
|
614
568
|
}
|
|
615
569
|
|
|
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
570
|
function projectIndexedSegment(centerline, segmentId, position) {
|
|
684
571
|
const ax = centerline.startX[segmentId];
|
|
685
572
|
const ay = centerline.startY[segmentId];
|
|
@@ -733,134 +620,6 @@ function segmentFromIndex(centerline, segmentId) {
|
|
|
733
620
|
};
|
|
734
621
|
}
|
|
735
622
|
|
|
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
623
|
function seedRayTraceFromNearbyCache(
|
|
865
624
|
index,
|
|
866
625
|
grid,
|