@inventure71/paddockjs 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +146 -1
  2. package/INSTALL_AND_UPDATE.md +15 -4
  3. package/README.md +84 -562
  4. package/docs/architecture.md +28 -18
  5. package/docs/composable-layouts.md +77 -0
  6. package/docs/custom_model_controller.md +4 -4
  7. package/docs/data-helpers.md +50 -0
  8. package/docs/data_contract.md +9 -7
  9. package/docs/getting-started.md +75 -0
  10. package/docs/headless-environment.md +68 -0
  11. package/docs/index.md +17 -4
  12. package/docs/rules.md +6 -2
  13. package/docs/system_specs.md +49 -6
  14. package/docs/theming.md +64 -0
  15. package/docs/training.md +20 -14
  16. package/docs/troubleshooting.md +76 -0
  17. package/docs/upgrading-hosts-to-3.md +42 -0
  18. package/examples/python/README.md +6 -0
  19. package/examples/python/base_policy_server.py +19 -0
  20. package/package.json +10 -3
  21. package/src/api/PaddockSimulatorController.js +78 -22
  22. package/src/api/runtimeTheme.js +59 -0
  23. package/src/app/F1SimulatorApp.js +24 -3
  24. package/src/app/readouts/timingTowerRenderer.js +203 -42
  25. package/src/app/runtime/frameLoop.js +5 -1
  26. package/src/config/defaultOptions.js +8 -1
  27. package/src/config/themeOptions.js +5 -0
  28. package/src/data/index.d.ts +212 -0
  29. package/src/data/index.js +14 -0
  30. package/src/environment/episode.js +11 -1
  31. package/src/environment/metrics.js +59 -16
  32. package/src/environment/observationVector.js +50 -33
  33. package/src/environment/observations.js +89 -20
  34. package/src/environment/runtime.js +41 -0
  35. package/src/environment/sensors/carRays.js +48 -37
  36. package/src/environment/sensors/index.js +501 -97
  37. package/src/environment/sensors/indexedRayBands.js +134 -46
  38. package/src/environment/sensors/rayBandTrace.js +1171 -0
  39. package/src/environment/sensors/rayChannels.js +108 -0
  40. package/src/environment/sensors/rayConfig.js +71 -25
  41. package/src/environment/sensors/rayGeometry.js +23 -17
  42. package/src/environment/sensors/sensorTargets.js +67 -1
  43. package/src/environment/sensors/surfaceRays.js +130 -89
  44. package/src/environment/sensors/trackRays.js +115 -1
  45. package/src/index.d.ts +43 -0
  46. package/src/index.js +27 -2
  47. package/src/simulation/collisionGeometry.js +260 -96
  48. package/src/simulation/pit/pitFlow.js +21 -9
  49. package/src/simulation/pit/pitQueue.js +5 -3
  50. package/src/simulation/pit/pitRouteMovement.js +9 -4
  51. package/src/simulation/pit/pitRouting.js +77 -42
  52. package/src/simulation/pit/pitState.js +1 -0
  53. package/src/simulation/pit/pitTireService.js +7 -3
  54. package/src/simulation/race/raceFinish.js +25 -8
  55. package/src/simulation/race/raceOrder.js +189 -32
  56. package/src/simulation/race/raceProgress.js +129 -37
  57. package/src/simulation/race/raceSetup.js +2 -0
  58. package/src/simulation/race/raceStep.js +25 -13
  59. package/src/simulation/raceSimulation.js +20 -17
  60. package/src/simulation/rules/collisionSteward.js +40 -23
  61. package/src/simulation/rules/rulesReview.js +77 -56
  62. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  63. package/src/simulation/timing/drsTiming.js +143 -30
  64. package/src/simulation/timing/gapEstimation.js +25 -3
  65. package/src/simulation/timing/lapTelemetry.js +115 -36
  66. package/src/simulation/timing/raceTiming.js +5 -1
  67. package/src/simulation/timing/sectorPerformance.js +104 -26
  68. package/src/simulation/timing/timingHistory.js +229 -14
  69. package/src/simulation/timing/timingLines.js +65 -7
  70. package/src/simulation/track/pitLaneLayout.js +23 -0
  71. package/src/simulation/track/pitLaneState.js +218 -19
  72. package/src/simulation/track/spatialQueries.js +67 -11
  73. package/src/simulation/track/trackMath.js +91 -24
  74. package/src/simulation/track/trackModel.js +2 -0
  75. package/src/simulation/track/trackQueryIndex.js +1334 -100
  76. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  77. package/src/simulation/track/trackQueryScratch.js +7 -0
  78. package/src/simulation/track/trackQueryStats.js +25 -0
  79. package/src/simulation/track/trackSnapshotJson.js +139 -0
  80. package/src/simulation/track/trackStatePolicy.js +249 -1
  81. package/src/simulation/vehicle/contactResolution.js +96 -59
  82. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  83. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  84. package/src/simulation/vehicle/runoffResponse.js +54 -29
  85. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  86. package/src/simulation/vehicle/vehiclePhysics.js +42 -27
  87. package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
  88. package/src/simulation/vehicle/vehicleState.js +6 -0
  89. package/src/simulation/vehicle/wheelSurface.js +807 -92
  90. package/src/styles.css +497 -497
@@ -3,48 +3,222 @@ import { normalizeRayOptions } from './rayConfig.js';
3
3
  import { degreesToRadians, getCarRayOrigin } from './rayGeometry.js';
4
4
  import { isSelfCarTarget, rayDetectableTargetsForSnapshot } from './sensorTargets.js';
5
5
  import { createTrackMiss, createTrackRayContext, estimateTrackHit } from './trackRays.js';
6
- import { createSurfaceMiss, estimateSurfaceHits, requestedSurfaceChannels } from './surfaceRays.js';
6
+ import { createSurfaceMiss, estimateSurfaceHits } from './surfaceRays.js';
7
7
  import { canUseBatchTrainingRayApproximation } from './rayGuards.js';
8
+ import { traceIndexedRayBands } from './rayBandTrace.js';
9
+ import { createRayChannelFlags, writeRayChannelFlags } from './rayChannels.js';
10
+ import { pitOverrideAllowedForCar } from '../../simulation/track/trackStatePolicy.js';
8
11
 
9
12
  export { buildNearbyCars } from './nearbyCars.js';
10
13
  export { DEFAULT_RAY_ANGLES_DEGREES } from './rayDefaults.js';
11
- export { normalizeRayOptions, RAY_CHANNELS, RAY_LAYOUT_PRESETS } from './rayConfig.js';
14
+ export { RAY_CHANNELS } from './rayChannels.js';
15
+ export { normalizeRayOptions, RAY_LAYOUT_PRESETS } from './rayConfig.js';
12
16
  export { getCarRayOrigin, getCarRayVector } from './rayGeometry.js';
13
17
 
14
- export function createRayBatchContext(snapshot) {
15
- return {
16
- rayTargets: rayDetectableTargetsForSnapshot(snapshot),
18
+ const CHANNEL_CONTEXT = Symbol('rayChannelContext');
19
+
20
+ export function createRayBatchContext(snapshot, { scratch = false } = {}) {
21
+ const contextScratch = scratch === true
22
+ ? {}
23
+ : scratch && typeof scratch === 'object'
24
+ ? scratch
25
+ : null;
26
+ const context = {
27
+ rayTargets: rayDetectableTargetsForSnapshot(snapshot, contextScratch),
17
28
  };
29
+ if (contextScratch) context.scratch = contextScratch;
30
+ return context;
18
31
  }
19
32
 
20
- export function buildRaySensors(car, snapshot, rayOptions = {}, batchContext = null) {
33
+ function getRayScratch(batchContext) {
34
+ return batchContext?.scratch ?? null;
35
+ }
36
+
37
+ function getNormalizedRayOptions(rayOptions, scratch) {
38
+ if (!scratch || !rayOptions || typeof rayOptions !== 'object') return normalizeRayOptions(rayOptions);
39
+ if (rayOptions?.rays && rayOptions?.channels && rayOptions?.anglesDegrees) {
40
+ return normalizeRayOptions(rayOptions);
41
+ }
42
+ const cache = scratch.normalizedRayOptionsBySource ?? new WeakMap();
43
+ scratch.normalizedRayOptionsBySource = cache;
44
+ const cached = cache.get(rayOptions);
45
+ if (cached) return cached;
21
46
  const normalized = normalizeRayOptions(rayOptions);
22
- if (!normalized.enabled) return [];
23
- if (car?.destroyed || car?.outOfRace) {
24
- return buildInactiveCarRays(normalized);
47
+ cache.set(rayOptions, normalized);
48
+ return normalized;
49
+ }
50
+
51
+ function getRayOrigin(car, scratch) {
52
+ if (!scratch) return getCarRayOrigin(car);
53
+ const origin = scratch.origin ?? {};
54
+ scratch.origin = origin;
55
+ origin.x = car.x;
56
+ origin.y = car.y;
57
+ return origin;
58
+ }
59
+
60
+ function getFilteredCarTargets(car, snapshot, batchContext, scratch) {
61
+ const targets = batchContext?.rayTargets ?? rayDetectableTargetsForSnapshot(snapshot);
62
+ if (!scratch) return targets.filter((target) => !isSelfCarTarget(car, target));
63
+ const filtered = scratch.carTargets ?? [];
64
+ scratch.carTargets = filtered;
65
+ let count = 0;
66
+ for (let index = 0; index < targets.length; index += 1) {
67
+ const target = targets[index];
68
+ if (isSelfCarTarget(car, target)) continue;
69
+ filtered[count] = target;
70
+ count += 1;
25
71
  }
26
- const origin = getCarRayOrigin(car);
27
- const usesTrackContext = normalized.channels.includes('roadEdge') ||
28
- requestedSurfaceChannels(normalized.channels).length > 0;
29
- const trackContext = !usesTrackContext
30
- ? null
31
- : createTrackRayContext(car, snapshot, origin, normalized.precision);
32
- const carTargets = normalized.channels.includes('car')
33
- ? (batchContext?.rayTargets ?? rayDetectableTargetsForSnapshot(snapshot)).filter((target) => !isSelfCarTarget(car, target))
34
- : [];
72
+ filtered.length = count;
73
+ return filtered;
74
+ }
35
75
 
36
- if (canUseFastBatchTrainingRays(car, snapshot, trackContext)) {
37
- return buildFastBatchTrainingRays(car, snapshot, normalized, origin, carTargets, trackContext);
76
+ function prepareScratchArray(scratch, arrayKey, poolKey, count, createItem) {
77
+ if (!scratch) return null;
78
+ const values = scratch[arrayKey] ?? [];
79
+ const pool = scratch[poolKey] ?? [];
80
+ scratch[arrayKey] = values;
81
+ scratch[poolKey] = pool;
82
+ values.length = count;
83
+ for (let index = 0; index < count; index += 1) {
84
+ const item = pool[index] ?? createItem();
85
+ pool[index] = item;
86
+ values[index] = item;
38
87
  }
88
+ return values;
89
+ }
39
90
 
40
- return normalized.rays.map((ray) => {
41
- const angleDegrees = ray.angleDegrees;
42
- const vector = {
43
- x: Math.cos((car.heading ?? 0) + degreesToRadians(angleDegrees)),
44
- y: Math.sin((car.heading ?? 0) + degreesToRadians(angleDegrees)),
91
+ function getScratchRayVector(scratch, index, car, angleRadians) {
92
+ if (!scratch) {
93
+ return {
94
+ x: Math.cos((car.heading ?? 0) + angleRadians),
95
+ y: Math.sin((car.heading ?? 0) + angleRadians),
45
96
  };
46
- const sharedRayQuery = {};
47
- const roadEdge = normalized.channels.includes('roadEdge')
97
+ }
98
+ const vectors = scratch.rayVectors ?? [];
99
+ scratch.rayVectors = vectors;
100
+ const vector = vectors[index] ?? {};
101
+ vectors[index] = vector;
102
+ vector.x = Math.cos((car.heading ?? 0) + angleRadians);
103
+ vector.y = Math.sin((car.heading ?? 0) + angleRadians);
104
+ return vector;
105
+ }
106
+
107
+ function getSharedRayQuery(scratch, index) {
108
+ if (!scratch) return {};
109
+ const queries = scratch.sharedRayQueries ?? [];
110
+ scratch.sharedRayQueries = queries;
111
+ const query = queries[index] ?? {};
112
+ queries[index] = query;
113
+ query.carHitResult = query.carHitResult ?? {};
114
+ query.surfaceBoundaries = null;
115
+ query.trackBandBoundaries = null;
116
+ query.boundaryStateCache = null;
117
+ query.sampleStateCache = null;
118
+ return query;
119
+ }
120
+
121
+ function getCarRayStats(scratch) {
122
+ if (!scratch) return null;
123
+ const stats = scratch.carRayStats ?? {
124
+ callerVectorCount: 0,
125
+ computedVectorCount: 0,
126
+ resultTargetCount: 0,
127
+ };
128
+ scratch.carRayStats = stats;
129
+ return stats;
130
+ }
131
+
132
+ function estimateCarHitForRay(car, snapshot, ray, origin, vector, carTargets, sharedRayQuery, scratch) {
133
+ return estimateCarHit(car, snapshot, ray.angleDegrees, ray.lengthMeters, origin, carTargets, {
134
+ rayVector: vector,
135
+ resultTarget: sharedRayQuery?.carHitResult,
136
+ stats: getCarRayStats(scratch),
137
+ });
138
+ }
139
+
140
+ function getRayBandTraceStats(scratch) {
141
+ if (!scratch) return null;
142
+ const stats = scratch.rayBandTraceStats ?? {
143
+ directPathCount: 0,
144
+ sampledPathCount: 0,
145
+ fallbackCount: 0,
146
+ channelSetAllocations: 0,
147
+ };
148
+ scratch.rayBandTraceStats = stats;
149
+ return stats;
150
+ }
151
+
152
+ function createRayChannelContext(normalized) {
153
+ if (normalized?.[CHANNEL_CONTEXT]) return normalized[CHANNEL_CONTEXT];
154
+ const channels = writeRayChannelFlags(createRayChannelFlags(), normalized.channels);
155
+ const context = {
156
+ channels,
157
+ surfaceChannels: channels.surfaceChannels,
158
+ hasRoadEdge: channels.hasRoadEdge,
159
+ hasCar: channels.hasCar,
160
+ hasRoadOrSurface: channels.hasRoadEdge || channels.surfaceChannels.length > 0,
161
+ };
162
+ if (normalized) {
163
+ Object.defineProperty(normalized, CHANNEL_CONTEXT, {
164
+ value: context,
165
+ enumerable: false,
166
+ });
167
+ }
168
+ return context;
169
+ }
170
+
171
+ function estimateRayBands({
172
+ car,
173
+ snapshot,
174
+ ray,
175
+ angleDegrees,
176
+ origin,
177
+ vector,
178
+ normalized,
179
+ channelContext,
180
+ trackContext,
181
+ sharedRayQuery,
182
+ scratch,
183
+ }) {
184
+ const stats = getRayBandTraceStats(scratch);
185
+ const canUseRayBandTrace = channelContext.hasRoadOrSurface &&
186
+ trackContext?.originState;
187
+ if (canUseRayBandTrace) {
188
+ const direct = traceIndexedRayBands({
189
+ track: snapshot.track,
190
+ origin,
191
+ vector,
192
+ lengthMeters: ray.lengthMeters,
193
+ originState: trackContext.originState,
194
+ channels: channelContext.channels,
195
+ precision: normalized.precision,
196
+ sharedRayQuery,
197
+ stats,
198
+ validateBoundaries: car?.interaction?.profile !== 'batch-training',
199
+ allowPitOverride: car?.interaction?.profile === 'batch-training'
200
+ ? false
201
+ : pitOverrideAllowedForCar(car),
202
+ mainTrackOnly: car?.interaction?.profile === 'batch-training',
203
+ });
204
+ if (direct.available) {
205
+ if (stats) {
206
+ if (direct.path === 'sampled') stats.sampledPathCount += 1;
207
+ else stats.directPathCount += 1;
208
+ }
209
+ return {
210
+ roadEdge: direct.roadEdge,
211
+ surfaceHits: {
212
+ kerb: direct.kerb,
213
+ illegalSurface: direct.illegalSurface,
214
+ },
215
+ };
216
+ }
217
+ if (stats) stats.fallbackCount += 1;
218
+ }
219
+
220
+ return {
221
+ roadEdge: channelContext.hasRoadEdge
48
222
  ? estimateTrackHit(
49
223
  car,
50
224
  snapshot,
@@ -53,47 +227,297 @@ export function buildRaySensors(car, snapshot, rayOptions = {}, batchContext = n
53
227
  trackContext,
54
228
  { precision: normalized.precision, sharedRayQuery },
55
229
  )
56
- : createTrackMiss(ray.lengthMeters);
57
- const carHit = normalized.channels.includes('car')
58
- ? estimateCarHit(car, snapshot, angleDegrees, ray.lengthMeters, origin, carTargets)
230
+ : createTrackMiss(ray.lengthMeters),
231
+ surfaceHits: estimateSurfaceHits(
232
+ car,
233
+ snapshot,
234
+ ray,
235
+ origin,
236
+ vector,
237
+ channelContext.surfaceChannels,
238
+ trackContext,
239
+ { precision: normalized.precision, sharedRayQuery },
240
+ ),
241
+ };
242
+ }
243
+
244
+ function writeRichRay(target, ray, angleRadians, roadEdge, carHit, surfaceHits, channels) {
245
+ target.id = ray.id;
246
+ target.angleDegrees = ray.angleDegrees;
247
+ target.angleRadians = angleRadians;
248
+ target.lengthMeters = ray.lengthMeters;
249
+ target.roadEdge = writeTrackRayHit(target.roadEdge ?? {}, roadEdge);
250
+ target.track = target.roadEdge;
251
+ target.kerb = channels.hasKerb
252
+ ? writeSurfaceRayHit(target.kerb ?? {}, surfaceHits.kerb ?? createSurfaceMiss(ray.lengthMeters))
253
+ : writeSurfaceRayHit(target.kerb ?? {}, createSurfaceMiss(ray.lengthMeters));
254
+ target.illegalSurface = channels.hasIllegalSurface
255
+ ? writeSurfaceRayHit(target.illegalSurface ?? {}, surfaceHits.illegalSurface ?? createSurfaceMiss(ray.lengthMeters))
256
+ : writeSurfaceRayHit(target.illegalSurface ?? {}, createSurfaceMiss(ray.lengthMeters));
257
+ target.car = writeCarRayHit(target.car ?? {}, carHit);
258
+ return target;
259
+ }
260
+
261
+ function writeTrackRayHit(target, source) {
262
+ target.hit = Boolean(source?.hit);
263
+ target.distanceMeters = source?.distanceMeters;
264
+ target.kind = source?.kind ?? null;
265
+ return target;
266
+ }
267
+
268
+ function writeSurfaceRayHit(target, source) {
269
+ target.hit = Boolean(source?.hit);
270
+ target.distanceMeters = source?.distanceMeters;
271
+ target.surface = source?.surface ?? null;
272
+ return target;
273
+ }
274
+
275
+ function writeCarRayHit(target, source) {
276
+ target.hit = Boolean(source?.hit);
277
+ target.distanceMeters = source?.distanceMeters;
278
+ target.driverId = source?.driverId ?? null;
279
+ target.targetId = source?.targetId ?? null;
280
+ target.targetType = source?.targetType ?? null;
281
+ target.relativeSpeedKph = source?.relativeSpeedKph ?? 0;
282
+ return target;
283
+ }
284
+
285
+ export function buildRaySensors(car, snapshot, rayOptions = {}, batchContext = null) {
286
+ const scratch = getRayScratch(batchContext);
287
+ const normalized = getNormalizedRayOptions(rayOptions, scratch);
288
+ if (!normalized.enabled) return [];
289
+ const channelContext = createRayChannelContext(normalized);
290
+ if (car?.destroyed || car?.outOfRace) {
291
+ return buildInactiveCarRays(normalized, scratch);
292
+ }
293
+ const origin = getRayOrigin(car, scratch);
294
+ const usesTrackContext = channelContext.hasRoadOrSurface;
295
+ const trackContext = !usesTrackContext
296
+ ? null
297
+ : createTrackRayContext(car, snapshot, origin, normalized.precision);
298
+ const carTargets = channelContext.hasCar
299
+ ? getFilteredCarTargets(car, snapshot, batchContext, scratch)
300
+ : [];
301
+
302
+ if (canUseFastBatchTrainingRays(car, snapshot, trackContext)) {
303
+ return buildFastBatchTrainingRays(car, snapshot, normalized, channelContext, origin, carTargets, trackContext, scratch);
304
+ }
305
+
306
+ const scratchRays = prepareScratchArray(scratch, 'rays', 'rayPool', normalized.rays.length, () => ({}));
307
+ const rays = scratchRays ?? normalized.rays.map(() => ({}));
308
+ for (let index = 0; index < normalized.rays.length; index += 1) {
309
+ const ray = normalized.rays[index];
310
+ const angleDegrees = ray.angleDegrees;
311
+ const angleRadians = degreesToRadians(angleDegrees);
312
+ const vector = getScratchRayVector(scratch, index, car, angleRadians);
313
+ const sharedRayQuery = getSharedRayQuery(scratch, index);
314
+ const { roadEdge, surfaceHits } = estimateRayBands({
315
+ car,
316
+ snapshot,
317
+ ray,
318
+ angleDegrees,
319
+ origin,
320
+ vector,
321
+ normalized,
322
+ channelContext,
323
+ trackContext,
324
+ sharedRayQuery,
325
+ scratch,
326
+ });
327
+ const carHit = channelContext.hasCar
328
+ ? estimateCarHitForRay(car, snapshot, ray, origin, vector, carTargets, sharedRayQuery, scratch)
59
329
  : createCarRayMiss(ray.lengthMeters);
60
- const surfaceHits = estimateSurfaceHits(
330
+
331
+ writeRichRay(rays[index], ray, angleRadians, roadEdge, carHit, surfaceHits, channelContext.channels);
332
+ }
333
+ return rays;
334
+ }
335
+
336
+ export function buildRaySensorVectorValues(car, snapshot, rayOptions = {}, batchContext = null) {
337
+ const scratch = getRayScratch(batchContext);
338
+ const normalized = getNormalizedRayOptions(rayOptions, scratch);
339
+ if (!normalized.enabled) return [];
340
+ const channelContext = createRayChannelContext(normalized);
341
+ const surfaceChannels = channelContext.surfaceChannels;
342
+ if (car?.destroyed || car?.outOfRace) {
343
+ return buildInactiveRayVectorValues(normalized, surfaceChannels, scratch);
344
+ }
345
+
346
+ const origin = getRayOrigin(car, scratch);
347
+ const usesTrackContext = channelContext.hasRoadOrSurface;
348
+ const trackContext = !usesTrackContext
349
+ ? null
350
+ : createTrackRayContext(car, snapshot, origin, normalized.precision);
351
+ const carTargets = channelContext.hasCar
352
+ ? getFilteredCarTargets(car, snapshot, batchContext, scratch)
353
+ : [];
354
+
355
+ const vectorValues = prepareScratchArray(scratch, 'rayVectorValues', 'rayVectorValuePool', normalized.rays.length, () => []);
356
+ const values = vectorValues ?? normalized.rays.map(() => []);
357
+ for (let index = 0; index < normalized.rays.length; index += 1) {
358
+ const ray = normalized.rays[index];
359
+ const angleRadians = degreesToRadians(ray.angleDegrees);
360
+ const vector = getScratchRayVector(scratch, index, car, angleRadians);
361
+ const sharedRayQuery = getSharedRayQuery(scratch, index);
362
+ const { roadEdge, surfaceHits } = estimateRayBands({
61
363
  car,
62
364
  snapshot,
63
365
  ray,
366
+ angleDegrees: ray.angleDegrees,
64
367
  origin,
65
368
  vector,
66
- normalized.channels,
369
+ normalized,
370
+ channelContext,
67
371
  trackContext,
68
- { precision: normalized.precision, sharedRayQuery },
69
- );
372
+ sharedRayQuery,
373
+ scratch,
374
+ });
375
+ const carHit = channelContext.hasCar
376
+ ? estimateCarHitForRay(car, snapshot, ray, origin, vector, carTargets, sharedRayQuery, scratch)
377
+ : createCarRayMiss(ray.lengthMeters);
378
+ writeRayVectorValues(values[index], ray.lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels);
379
+ }
380
+ return values;
381
+ }
70
382
 
71
- return {
72
- id: ray.id,
73
- angleDegrees,
74
- angleRadians: degreesToRadians(angleDegrees),
75
- lengthMeters: ray.lengthMeters,
383
+ export function appendRaySensorVectorValues(target, car, snapshot, rayOptions = {}, batchContext = null) {
384
+ const scratch = getRayScratch(batchContext);
385
+ const normalized = getNormalizedRayOptions(rayOptions, scratch);
386
+ if (!normalized.enabled) return target;
387
+ const channelContext = createRayChannelContext(normalized);
388
+ const surfaceChannels = channelContext.surfaceChannels;
389
+ if (car?.destroyed || car?.outOfRace) {
390
+ for (let index = 0; index < normalized.rays.length; index += 1) {
391
+ const ray = normalized.rays[index];
392
+ appendRayVectorValues(
393
+ target,
394
+ ray.lengthMeters,
395
+ createTrackMiss(ray.lengthMeters),
396
+ createCarRayMiss(ray.lengthMeters),
397
+ {},
398
+ surfaceChannels,
399
+ );
400
+ }
401
+ return target;
402
+ }
403
+
404
+ const origin = getRayOrigin(car, scratch);
405
+ const usesTrackContext = channelContext.hasRoadOrSurface;
406
+ const trackContext = !usesTrackContext
407
+ ? null
408
+ : createTrackRayContext(car, snapshot, origin, normalized.precision);
409
+ const carTargets = channelContext.hasCar
410
+ ? getFilteredCarTargets(car, snapshot, batchContext, scratch)
411
+ : [];
412
+
413
+ for (let index = 0; index < normalized.rays.length; index += 1) {
414
+ const ray = normalized.rays[index];
415
+ const angleRadians = degreesToRadians(ray.angleDegrees);
416
+ const vector = getScratchRayVector(scratch, index, car, angleRadians);
417
+ const sharedRayQuery = getSharedRayQuery(scratch, index);
418
+ const { roadEdge, surfaceHits } = estimateRayBands({
419
+ car,
420
+ snapshot,
421
+ ray,
422
+ angleDegrees: ray.angleDegrees,
423
+ origin,
424
+ vector,
425
+ normalized,
426
+ channelContext,
427
+ trackContext,
428
+ sharedRayQuery,
429
+ scratch,
430
+ });
431
+ const carHit = channelContext.hasCar
432
+ ? estimateCarHitForRay(car, snapshot, ray, origin, vector, carTargets, sharedRayQuery, scratch)
433
+ : createCarRayMiss(ray.lengthMeters);
434
+ appendRayVectorValues(target, ray.lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels);
435
+ }
436
+ return target;
437
+ }
438
+
439
+ function buildInactiveCarRays(normalized, scratch = null) {
440
+ const channelContext = createRayChannelContext(normalized);
441
+ const rays = prepareScratchArray(scratch, 'rays', 'rayPool', normalized.rays.length, () => ({})) ??
442
+ normalized.rays.map(() => ({}));
443
+ for (let index = 0; index < normalized.rays.length; index += 1) {
444
+ const ray = normalized.rays[index];
445
+ const roadEdge = createTrackMiss(ray.lengthMeters);
446
+ writeRichRay(
447
+ rays[index],
448
+ ray,
449
+ degreesToRadians(ray.angleDegrees),
76
450
  roadEdge,
77
- track: roadEdge,
78
- kerb: surfaceHits.kerb ?? createSurfaceMiss(ray.lengthMeters),
79
- illegalSurface: surfaceHits.illegalSurface ?? createSurfaceMiss(ray.lengthMeters),
80
- car: carHit,
81
- };
82
- });
451
+ createCarRayMiss(ray.lengthMeters),
452
+ {},
453
+ channelContext.channels,
454
+ );
455
+ }
456
+ return rays;
457
+ }
458
+
459
+ function createInactiveRayVectorValues(ray, surfaceChannels) {
460
+ return rayVectorValues(
461
+ ray.lengthMeters,
462
+ createTrackMiss(ray.lengthMeters),
463
+ createCarRayMiss(ray.lengthMeters),
464
+ {},
465
+ surfaceChannels,
466
+ );
467
+ }
468
+
469
+ function buildInactiveRayVectorValues(normalized, surfaceChannels, scratch = null) {
470
+ const values = prepareScratchArray(scratch, 'rayVectorValues', 'rayVectorValuePool', normalized.rays.length, () => []) ??
471
+ normalized.rays.map(() => []);
472
+ for (let index = 0; index < normalized.rays.length; index += 1) {
473
+ const ray = normalized.rays[index];
474
+ writeRayVectorValues(
475
+ values[index],
476
+ ray.lengthMeters,
477
+ createTrackMiss(ray.lengthMeters),
478
+ createCarRayMiss(ray.lengthMeters),
479
+ {},
480
+ surfaceChannels,
481
+ );
482
+ }
483
+ return values;
484
+ }
485
+
486
+ function rayVectorValues(lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels) {
487
+ return writeRayVectorValues([], lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels);
488
+ }
489
+
490
+ function writeRayVectorValues(values, lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels) {
491
+ values.length = 0;
492
+ appendRayVectorValues(values, lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels);
493
+ return values;
494
+ }
495
+
496
+ function appendRayVectorValues(values, lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels) {
497
+ values.push(
498
+ ratio(roadEdge.distanceMeters, lengthMeters),
499
+ roadEdge.hit ? 1 : 0,
500
+ roadEdge.kind === 'exit' ? 1 : 0,
501
+ roadEdge.kind === 'entry' ? 1 : 0,
502
+ ratio(carHit.distanceMeters, lengthMeters),
503
+ carHit.hit ? 1 : 0,
504
+ carHit.relativeSpeedKph / 200,
505
+ carHit.targetType === 'replayGhost' ? 1 : 0,
506
+ );
507
+ for (let index = 0; index < surfaceChannels.length; index += 1) {
508
+ const channel = surfaceChannels[index];
509
+ const hit = surfaceHits[channel] ?? createSurfaceMiss(lengthMeters);
510
+ values.push(
511
+ ratio(hit.distanceMeters, lengthMeters),
512
+ hit.hit ? 1 : 0,
513
+ );
514
+ }
515
+ return values;
83
516
  }
84
517
 
85
- function buildInactiveCarRays(normalized) {
86
- return normalized.rays.map((ray) => ({
87
- id: ray.id,
88
- angleDegrees: ray.angleDegrees,
89
- angleRadians: degreesToRadians(ray.angleDegrees),
90
- lengthMeters: ray.lengthMeters,
91
- roadEdge: createTrackMiss(ray.lengthMeters),
92
- track: createTrackMiss(ray.lengthMeters),
93
- kerb: createSurfaceMiss(ray.lengthMeters),
94
- illegalSurface: createSurfaceMiss(ray.lengthMeters),
95
- car: createCarRayMiss(ray.lengthMeters),
96
- }));
518
+ function ratio(value, max) {
519
+ const finite = Number.isFinite(value) ? value : max;
520
+ return Math.max(0, Math.min(1, finite / Math.max(1e-9, max)));
97
521
  }
98
522
 
99
523
  function canUseFastBatchTrainingRays(car, snapshot, trackContext) {
@@ -106,52 +530,32 @@ function canUseFastBatchTrainingRays(car, snapshot, trackContext) {
106
530
  canUseBatchTrainingRayApproximation(snapshot.track, trackContext.originState);
107
531
  }
108
532
 
109
- function buildFastBatchTrainingRays(car, snapshot, normalized, origin, carTargets, trackContext) {
110
- const channels = new Set(normalized.channels);
111
-
112
- return normalized.rays.map((ray) => {
533
+ function buildFastBatchTrainingRays(car, snapshot, normalized, channelContext, origin, carTargets, trackContext, scratch = null) {
534
+ const rays = prepareScratchArray(scratch, 'rays', 'rayPool', normalized.rays.length, () => ({})) ??
535
+ normalized.rays.map(() => ({}));
536
+ for (let index = 0; index < normalized.rays.length; index += 1) {
537
+ const ray = normalized.rays[index];
113
538
  const angleRadians = degreesToRadians(ray.angleDegrees);
114
- const heading = (car.heading ?? 0) + angleRadians;
115
- const vector = { x: Math.cos(heading), y: Math.sin(heading) };
116
- const sharedRayQuery = {};
117
- const roadEdge = channels.has('roadEdge')
118
- ? estimateTrackHit(
119
- car,
120
- snapshot,
121
- ray.angleDegrees,
122
- ray.lengthMeters,
123
- trackContext,
124
- { precision: normalized.precision, sharedRayQuery },
125
- )
126
- : createTrackMiss(ray.lengthMeters);
127
- const carHit = channels.has('car') && carTargets.length > 0
128
- ? estimateCarHit(car, snapshot, ray.angleDegrees, ray.lengthMeters, origin, carTargets)
129
- : createCarRayMiss(ray.lengthMeters);
130
- const surfaceHits = estimateSurfaceHits(
539
+ const vector = getScratchRayVector(scratch, index, car, angleRadians);
540
+ const sharedRayQuery = getSharedRayQuery(scratch, index);
541
+ const { roadEdge, surfaceHits } = estimateRayBands({
131
542
  car,
132
543
  snapshot,
133
544
  ray,
545
+ angleDegrees: ray.angleDegrees,
134
546
  origin,
135
547
  vector,
136
- normalized.channels,
548
+ normalized,
549
+ channelContext,
137
550
  trackContext,
138
- { precision: normalized.precision, sharedRayQuery },
139
- );
551
+ sharedRayQuery,
552
+ scratch,
553
+ });
554
+ const carHit = channelContext.hasCar && carTargets.length > 0
555
+ ? estimateCarHitForRay(car, snapshot, ray, origin, vector, carTargets, sharedRayQuery, scratch)
556
+ : createCarRayMiss(ray.lengthMeters);
140
557
 
141
- return {
142
- id: ray.id,
143
- angleDegrees: ray.angleDegrees,
144
- angleRadians,
145
- lengthMeters: ray.lengthMeters,
146
- roadEdge,
147
- track: roadEdge,
148
- kerb: channels.has('kerb')
149
- ? (surfaceHits.kerb ?? createSurfaceMiss(ray.lengthMeters))
150
- : createSurfaceMiss(ray.lengthMeters),
151
- illegalSurface: channels.has('illegalSurface')
152
- ? (surfaceHits.illegalSurface ?? createSurfaceMiss(ray.lengthMeters))
153
- : createSurfaceMiss(ray.lengthMeters),
154
- car: carHit,
155
- };
156
- });
558
+ writeRichRay(rays[index], ray, angleRadians, roadEdge, carHit, surfaceHits, channelContext.channels);
559
+ }
560
+ return rays;
157
561
  }