@inventure71/paddockjs 4.1.0 → 4.3.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 (257) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/INSTALL_AND_UPDATE.md +31 -5
  3. package/README.md +34 -3
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +111 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +32 -16
  9. package/docs/composable-layouts.md +2 -0
  10. package/docs/concepts.md +12 -1
  11. package/docs/custom_model_controller.md +1 -1
  12. package/docs/data_contract.md +33 -10
  13. package/docs/data_model.md +4 -2
  14. package/docs/efficiency_cleanup_2026-10-13.md +80 -0
  15. package/docs/getting-started.md +3 -0
  16. package/docs/grid_walk.md +80 -0
  17. package/docs/index.md +5 -2
  18. package/docs/keyboard_driving_fix.md +118 -0
  19. package/docs/limit_driving_2026-10-14.md +88 -0
  20. package/docs/loading.md +141 -0
  21. package/docs/maintainability_audit_2026-09-08.md +146 -0
  22. package/docs/racing_ai_improvements.md +145 -0
  23. package/docs/rules.md +43 -13
  24. package/docs/sense_contract.md +4 -1
  25. package/docs/system_specs.md +40 -12
  26. package/docs/testing-reliability-guide.md +67 -0
  27. package/docs/v11_improvements.md +108 -0
  28. package/package.json +23 -15
  29. package/src/api/MountedSimulatorComponents.js +101 -0
  30. package/src/api/PaddockSimulatorController.js +56 -81
  31. package/src/app/BrowserExpertAdapter.js +0 -6
  32. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  33. package/src/app/ExternalTrackSnapshot.js +50 -0
  34. package/src/app/F1SimulatorApp.js +242 -297
  35. package/src/app/HostLifecycleEmitter.js +93 -0
  36. package/src/app/banners/raceDataBannerController.js +9 -4
  37. package/src/app/camera/cameraConstants.js +1 -1
  38. package/src/app/camera/cameraController.js +10 -13
  39. package/src/app/camera/cameraGeometry.js +1 -1
  40. package/src/app/controls/SimulatorViewControls.js +45 -0
  41. package/src/app/controls/workbenchBindings.js +64 -0
  42. package/src/app/controls/workbenchDisclosure.js +41 -0
  43. package/src/app/domBindings.js +21 -13
  44. package/src/app/gridWalk/GridWalkController.js +180 -0
  45. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  46. package/src/app/gridWalk/gridWalkControls.js +24 -0
  47. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  48. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  49. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  50. package/src/app/gridWalk/gridWalkNavigation.js +99 -0
  51. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  52. package/src/app/gridWalk/gridWalkSession.js +50 -0
  53. package/src/app/layoutSupport.js +2 -0
  54. package/src/app/raceOverlayClearanceSupport.js +21 -7
  55. package/src/app/readouts/carOverviewRenderer.js +9 -4
  56. package/src/app/readouts/readoutFormatters.js +1 -8
  57. package/src/app/readouts/telemetryRenderer.js +1 -3
  58. package/src/app/readouts/timingTowerRenderer.js +48 -50
  59. package/src/app/rendering/appAssets.js +8 -10
  60. package/src/app/rendering/carRenderer.js +27 -79
  61. package/src/app/rendering/displayUtils.js +0 -6
  62. package/src/app/rendering/expertSensorRenderer.js +1 -1
  63. package/src/app/rendering/replayGhostRenderer.js +4 -1
  64. package/src/app/rendering/trackRenderer.js +2 -2
  65. package/src/app/runtime/frameLoop.js +18 -17
  66. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  67. package/src/app/runtime/runtimeTiming.js +4 -1
  68. package/src/app/runtime/runtimeVisibility.js +7 -2
  69. package/src/config/cssValues.js +1 -1
  70. package/src/config/defaultOptions.js +12 -173
  71. package/src/config/restartOptions.js +1 -0
  72. package/src/config/runtimeSeed.js +10 -0
  73. package/src/config/simulatorDefaults.js +106 -0
  74. package/src/config/telemetryModules.js +20 -0
  75. package/src/config/theme/cssTokens.js +85 -0
  76. package/src/config/theme/themeCss.js +72 -0
  77. package/src/config/theme/themeInputs.js +155 -0
  78. package/src/config/theme/themeResolver.js +172 -0
  79. package/src/config/theme/themeSelectors.js +69 -0
  80. package/src/config/theme/themeTokens.js +257 -0
  81. package/src/config/theme/themeUtils.js +15 -0
  82. package/src/config/themeOptions.js +14 -830
  83. package/src/config/timingGapMode.js +0 -2
  84. package/src/config/uiOptions.js +53 -0
  85. package/src/data/championship.js +16 -158
  86. package/src/data/championshipDefaults.js +87 -0
  87. package/src/data/driverData.js +10 -20
  88. package/src/data/entryIdentity.js +67 -0
  89. package/src/data/entryPresentation.js +23 -0
  90. package/src/data/entryTypes.d.ts +160 -0
  91. package/src/data/index.d.ts +12 -195
  92. package/src/data/ratingScale.js +14 -0
  93. package/src/data/teamTypes.d.ts +23 -0
  94. package/src/data/trackTypes.d.ts +101 -0
  95. package/src/data/vehicleData.js +12 -23
  96. package/src/environment/actions.js +3 -2
  97. package/src/environment/controllerLoop.js +57 -44
  98. package/src/environment/controllerScheduler.js +30 -0
  99. package/src/environment/driverObservations.js +260 -0
  100. package/src/environment/driverReset.js +51 -0
  101. package/src/environment/episode.js +0 -1
  102. package/src/environment/evaluation.js +6 -90
  103. package/src/environment/evaluationTracker.js +100 -0
  104. package/src/environment/events.js +23 -0
  105. package/src/environment/index.d.ts +35 -91
  106. package/src/environment/metrics.js +3 -17
  107. package/src/environment/observationSchema.js +104 -0
  108. package/src/environment/observationVector.js +16 -115
  109. package/src/environment/observations.js +16 -316
  110. package/src/environment/options.js +5 -24
  111. package/src/environment/renderFrames.js +21 -0
  112. package/src/environment/resetOptions.js +41 -0
  113. package/src/environment/results.js +125 -0
  114. package/src/environment/rewards.js +10 -16
  115. package/src/environment/runtime.js +17 -300
  116. package/src/environment/scenarioOptions.js +90 -0
  117. package/src/environment/scenarioPlacements.js +70 -0
  118. package/src/environment/scenarioPresets.js +78 -0
  119. package/src/environment/scenarios.js +5 -236
  120. package/src/environment/scratch.js +18 -0
  121. package/src/environment/sensorOptions.js +20 -0
  122. package/src/environment/sensors/index.js +2 -556
  123. package/src/environment/sensors/indexedRayBands.js +0 -18
  124. package/src/environment/sensors/nearbyCars.js +18 -15
  125. package/src/environment/sensors/opponentRadar.js +3 -2
  126. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  127. package/src/environment/sensors/rayBandTrace.js +1 -37
  128. package/src/environment/sensors/rayBatch.js +81 -0
  129. package/src/environment/sensors/rayConfig.js +26 -0
  130. package/src/environment/sensors/rayGeometry.js +0 -8
  131. package/src/environment/sensors/rayGuards.js +0 -6
  132. package/src/environment/sensors/rayOutputs.js +75 -0
  133. package/src/environment/sensors/rayQuery.js +110 -0
  134. package/src/environment/sensors/rayScratch.js +146 -0
  135. package/src/environment/sensors/sensorTargets.js +12 -4
  136. package/src/environment/sensors/surfaceRays.js +6 -61
  137. package/src/environment/sensors/trackRays.js +7 -80
  138. package/src/environment/simulationSetup.js +30 -0
  139. package/src/environment/specs.js +15 -109
  140. package/src/index.d.ts +43 -202
  141. package/src/index.js +28 -6
  142. package/src/placeholder/index.js +1 -8
  143. package/src/rendering/displayLifecycle.js +6 -0
  144. package/src/rendering/proceduralTrackAsset.js +1 -1
  145. package/src/rendering/renderSnapshot.js +3 -0
  146. package/src/rendering/track/finishLineRenderer.js +1 -1
  147. package/src/rendering/track/gridRenderer.js +1 -1
  148. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  149. package/src/rendering/track/pitLaneRenderer.js +1 -1
  150. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  151. package/src/rendering/track/trackRenderConstants.js +0 -1
  152. package/src/rendering/track/trackRenderGeometry.js +1 -7
  153. package/src/simulation/collisionGeometry.js +12 -20
  154. package/src/simulation/driver/advancedPathControls.js +53 -0
  155. package/src/simulation/driver/advancedRacePace.js +112 -0
  156. package/src/simulation/driver/advancedRacingControls.js +61 -0
  157. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  158. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  159. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  160. package/src/simulation/driver/arcadeRacePace.js +83 -0
  161. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  162. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  163. package/src/simulation/driver/driverControlConstants.js +0 -2
  164. package/src/simulation/driver/driverController.js +16 -5
  165. package/src/simulation/driver/edgeRecovery.js +2 -3
  166. package/src/simulation/driver/racingControls.js +10 -396
  167. package/src/simulation/driver/racingDriverProfile.js +48 -0
  168. package/src/simulation/driver/racingLinePlan.js +54 -20
  169. package/src/simulation/driver/rejoinControls.js +59 -112
  170. package/src/simulation/driver/safetyCarControls.js +12 -0
  171. package/src/simulation/driver/trafficScan.js +136 -27
  172. package/src/simulation/participants/participantInteractions.js +24 -15
  173. package/src/simulation/pit/pitCarPlacement.js +29 -0
  174. package/src/simulation/pit/pitFlow.js +8 -30
  175. package/src/simulation/pit/pitIntent.js +4 -3
  176. package/src/simulation/pit/pitOccupancy.js +2 -2
  177. package/src/simulation/pit/pitQueue.js +2 -18
  178. package/src/simulation/pit/pitRouteMovement.js +1 -36
  179. package/src/simulation/pit/pitRouting.js +5 -81
  180. package/src/simulation/pit/pitService.js +3 -32
  181. package/src/simulation/pit/pitServiceConstants.js +1 -0
  182. package/src/simulation/pit/pitServiceProgress.js +34 -0
  183. package/src/simulation/pit/pitSnapshots.js +8 -4
  184. package/src/simulation/pit/pitState.js +1 -5
  185. package/src/simulation/race/classification.js +1 -8
  186. package/src/simulation/race/raceFinish.js +3 -8
  187. package/src/simulation/race/raceOrder.js +2 -13
  188. package/src/simulation/race/raceProgress.js +6 -20
  189. package/src/simulation/race/raceSetup.js +1 -1
  190. package/src/simulation/race/raceStep.js +20 -11
  191. package/src/simulation/race/redFlag.js +2 -4
  192. package/src/simulation/race/safetyCar.js +5 -9
  193. package/src/simulation/race/stalledDnf.js +2 -4
  194. package/src/simulation/raceSimulation.js +6 -38
  195. package/src/simulation/replay/replayGhosts.js +44 -32
  196. package/src/simulation/rules/collisionSteward.js +0 -14
  197. package/src/simulation/rules/penaltyActions.js +37 -0
  198. package/src/simulation/rules/penaltyLedger.js +8 -1
  199. package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
  200. package/src/simulation/rules/rulesReview.js +20 -16
  201. package/src/simulation/rules/trackLimitsSteward.js +2 -2
  202. package/src/simulation/simMath.js +4 -0
  203. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  204. package/src/simulation/timing/drsState.js +8 -0
  205. package/src/simulation/timing/drsTiming.js +2 -4
  206. package/src/simulation/timing/lapTelemetry.js +7 -4
  207. package/src/simulation/timing/sectorPerformance.js +14 -6
  208. package/src/simulation/timing/timingLines.js +12 -6
  209. package/src/simulation/track/pitLaneAccess.js +2 -2
  210. package/src/simulation/track/pitLaneLayout.js +4 -135
  211. package/src/simulation/track/pitLaneModel.js +70 -0
  212. package/src/simulation/track/pitLaneState.js +0 -1
  213. package/src/simulation/track/pitServiceLayout.js +121 -0
  214. package/src/simulation/track/proceduralTrack.js +77 -0
  215. package/src/simulation/track/spatialQueries.js +76 -31
  216. package/src/simulation/track/trackBuilder.js +54 -0
  217. package/src/simulation/track/trackConstants.js +0 -22
  218. package/src/simulation/track/trackMath.js +4 -29
  219. package/src/simulation/track/trackModel.js +12 -240
  220. package/src/simulation/track/trackModelCache.js +105 -0
  221. package/src/simulation/track/trackNearestQueries.js +1218 -0
  222. package/src/simulation/track/trackQueryConstants.js +3 -0
  223. package/src/simulation/track/trackQueryConstruction.js +368 -0
  224. package/src/simulation/track/trackQueryIndex.js +12 -1933
  225. package/src/simulation/track/trackQuerySegments.js +174 -0
  226. package/src/simulation/track/trackQueryStats.js +12 -0
  227. package/src/simulation/track/trackRayQueries.js +165 -0
  228. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  229. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  230. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  231. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  232. package/src/simulation/vehicle/contactResolution.js +23 -9
  233. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  234. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  235. package/src/simulation/vehicle/runoffResponse.js +2 -4
  236. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  237. package/src/simulation/vehicle/vehicleGeometry.js +85 -103
  238. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  239. package/src/simulation/vehicle/vehiclePhysics.js +42 -196
  240. package/src/simulation/vehicle/vehicleSnapshots.js +18 -6
  241. package/src/simulation/vehicle/vehicleState.js +5 -3
  242. package/src/simulation/vehicle/wheelSurface.js +13 -432
  243. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  244. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  245. package/src/styles.css +173 -19
  246. package/src/ui/bannerTemplates.js +4 -2
  247. package/src/ui/cameraControlsTemplate.js +4 -2
  248. package/src/ui/gridWalkTemplate.js +29 -0
  249. package/src/ui/htmlEscaping.js +8 -0
  250. package/src/ui/raceCanvasTemplate.js +3 -2
  251. package/src/ui/shellTemplate.js +3 -0
  252. package/src/ui/telemetryDrawerTemplate.js +4 -2
  253. package/src/ui/telemetryTemplates.js +3 -22
  254. package/src/ui/templateUtils.js +2 -8
  255. package/src/ui/viewControlsTemplate.js +21 -0
  256. package/examples/train-basic-policy.mjs +0 -276
  257. package/examples/trainingData.mjs +0 -133
@@ -7,13 +7,13 @@ import { querySegmentNeighborhoodProjection, querySegmentNeighborhoodProjections
7
7
  import {
8
8
  ANALYTIC_TRACK_RAY_MAX_CURVATURE,
9
9
  DRIVER_RAY_REFINE_STEPS,
10
- PIT_CONNECTOR_RAY_FALLBACK_METERS,
11
10
  TRACK_RAY_REFINE_STEPS,
12
11
  TRACK_RAY_STEP_METERS,
13
12
  } from './rayDefaults.js';
14
13
  import { findIndexedRayBoundaryDistances } from './indexedRayBands.js';
15
14
  import { createRayChannelFlags, isLegalRaySurface, writeRayChannelFlags } from './rayChannels.js';
16
15
  import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
16
+ import { isNearPitConnector } from './pitConnectorProximity.js';
17
17
 
18
18
  const BOUNDARY_VALIDATION_EPSILON_METERS = 0.25;
19
19
  const LOCAL_SEGMENT_RECOVERY_BATCH_SIZE = 16;
@@ -67,7 +67,6 @@ export function traceIndexedRayBands({
67
67
  }
68
68
 
69
69
  const trackHalfWidth = track.width / 2;
70
- const kerbOuter = trackHalfWidth + (track.kerbWidth ?? 0);
71
70
  const boundaries = findIndexedSurfaceBoundaries(
72
71
  track,
73
72
  origin,
@@ -321,15 +320,6 @@ function directBoundaryFallbackReason({
321
320
  ) {
322
321
  return 'kerb-recovery';
323
322
  }
324
- if (
325
- channels.hasIllegalSurface &&
326
- originState.surface === 'barrier' &&
327
- boundaries.runoffOuterDistance == null &&
328
- Math.abs(originState.signedOffset) > kerbOuter &&
329
- !barrierOriginCanStayDirect
330
- ) {
331
- return 'illegal-surface-recovery';
332
- }
333
323
  return null;
334
324
  }
335
325
 
@@ -1090,14 +1080,6 @@ function isInsideTrackBorder(state, track, includePitLane = false) {
1090
1080
  return Math.abs(state?.crossTrackError ?? Infinity) <= track.width / 2;
1091
1081
  }
1092
1082
 
1093
- function createTrackMiss(lengthMeters) {
1094
- return writeTrackMiss({}, lengthMeters);
1095
- }
1096
-
1097
- function createSurfaceMiss(lengthMeters) {
1098
- return writeSurfaceMiss({}, lengthMeters);
1099
- }
1100
-
1101
1083
  function writeTrackMiss(target, lengthMeters) {
1102
1084
  target.hit = false;
1103
1085
  target.distanceMeters = lengthMeters;
@@ -1119,24 +1101,6 @@ function writeSurfaceHit(target, distanceMeters, surface) {
1119
1101
  return target;
1120
1102
  }
1121
1103
 
1122
- function isNearPitConnector(track, state) {
1123
- const pitLane = track.pitLane;
1124
- if (!pitLane?.enabled || !Number.isFinite(state?.distance)) return false;
1125
- const window = metersToSimUnits(PIT_CONNECTOR_RAY_FALLBACK_METERS);
1126
- const entryDistance = pitLane.entry?.trackDistance ?? pitLane.entry?.distanceFromStart;
1127
- const exitDistance = pitLane.exit?.trackDistance ?? pitLane.exit?.distanceFromStart;
1128
- return wrappedTrackDistance(state.distance, entryDistance, track.length) <= window ||
1129
- wrappedTrackDistance(state.distance, exitDistance, track.length) <= window;
1130
- }
1131
-
1132
- function wrappedTrackDistance(first, second, totalLength) {
1133
- if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(totalLength) || totalLength <= 0) {
1134
- return Infinity;
1135
- }
1136
- const delta = Math.abs(first - second);
1137
- return Math.min(delta, totalLength - delta);
1138
- }
1139
-
1140
1104
  function minFinite(...values) {
1141
1105
  let minimum = Infinity;
1142
1106
  for (let index = 0; index < values.length; index += 1) {
@@ -0,0 +1,81 @@
1
+ import { createCarRayMiss } from './carRays.js';
2
+ import { createTrackMiss, createTrackRayContext } from './trackRays.js';
3
+ import { canUseBatchTrainingRayApproximation } from './rayGuards.js';
4
+ import { degreesToRadians } from './rayGeometry.js';
5
+ import { createRayChannelContext, estimateCarHitForRay, estimateRayBands } from './rayQuery.js';
6
+ import { appendRayVectorValues, writeRayVectorValues, writeRichRay } from './rayOutputs.js';
7
+ import { getFilteredCarTargets, getNormalizedRayOptions, getRayOrigin, getRayScratch, getScratchRayVector, getSharedRayQuery, prepareScratchArray } from './rayScratch.js';
8
+
9
+ export function buildRaySensors(car, snapshot, rayOptions = {}, batchContext = null) {
10
+ return buildRayBatch(car, snapshot, rayOptions, batchContext, 'rich');
11
+ }
12
+
13
+ export function buildRaySensorVectorValues(car, snapshot, rayOptions = {}, batchContext = null) {
14
+ return buildRayBatch(car, snapshot, rayOptions, batchContext, 'vector');
15
+ }
16
+
17
+ export function appendRaySensorVectorValues(target, car, snapshot, rayOptions = {}, batchContext = null) {
18
+ return buildRayBatch(car, snapshot, rayOptions, batchContext, 'append', target);
19
+ }
20
+
21
+ function buildRayBatch(car, snapshot, rayOptions, batchContext, output, target = null) {
22
+ const scratch = getRayScratch(batchContext);
23
+ const normalized = getNormalizedRayOptions(rayOptions, scratch);
24
+ if (!normalized.enabled) return target ?? [];
25
+ const channelContext = createRayChannelContext(normalized);
26
+ const inactive = Boolean(car?.destroyed || car?.outOfRace);
27
+ const origin = inactive ? null : getRayOrigin(car, scratch);
28
+ const trackContext = !inactive && channelContext.hasRoadOrSurface
29
+ ? createTrackRayContext(car, snapshot, origin, normalized.precision)
30
+ : null;
31
+ const carTargets = !inactive && channelContext.hasCar
32
+ ? getFilteredCarTargets(car, snapshot, batchContext, scratch)
33
+ : [];
34
+ const skipEmptyCarQuery = output === 'rich' && canUseFastBatchTrainingRays(car, snapshot, trackContext) && carTargets.length === 0;
35
+ const values = target ?? (output === 'rich'
36
+ ? prepareScratchArray(scratch, 'rays', 'rayPool', normalized.rays.length, () => ({})) ?? normalized.rays.map(() => ({}))
37
+ : prepareScratchArray(scratch, 'rayVectorValues', 'rayVectorValuePool', normalized.rays.length, () => []) ?? normalized.rays.map(() => []));
38
+
39
+ for (let index = 0; index < normalized.rays.length; index += 1) {
40
+ const ray = normalized.rays[index];
41
+ const angleRadians = degreesToRadians(ray.angleDegrees);
42
+ const vector = inactive ? null : getScratchRayVector(scratch, index, car, angleRadians);
43
+ const sharedRayQuery = inactive ? null : getSharedRayQuery(scratch, index);
44
+ const { roadEdge, surfaceHits } = inactive
45
+ ? { roadEdge: createTrackMiss(ray.lengthMeters), surfaceHits: {} }
46
+ : estimateRayBands({
47
+ car,
48
+ snapshot,
49
+ ray,
50
+ angleDegrees: ray.angleDegrees,
51
+ origin,
52
+ vector,
53
+ normalized,
54
+ channelContext,
55
+ trackContext,
56
+ sharedRayQuery,
57
+ scratch,
58
+ });
59
+ const carHit = !inactive && channelContext.hasCar && !skipEmptyCarQuery
60
+ ? estimateCarHitForRay(car, snapshot, ray, origin, vector, carTargets, sharedRayQuery, scratch)
61
+ : createCarRayMiss(ray.lengthMeters);
62
+ if (output === 'rich') {
63
+ writeRichRay(values[index], ray, angleRadians, roadEdge, carHit, surfaceHits, channelContext.channels);
64
+ } else if (output === 'vector') {
65
+ writeRayVectorValues(values[index], ray.lengthMeters, roadEdge, carHit, surfaceHits, channelContext.surfaceChannels);
66
+ } else {
67
+ appendRayVectorValues(values, ray.lengthMeters, roadEdge, carHit, surfaceHits, channelContext.surfaceChannels);
68
+ }
69
+ }
70
+ return values;
71
+ }
72
+
73
+ function canUseFastBatchTrainingRays(car, snapshot, trackContext) {
74
+ return car?.interaction?.profile === 'batch-training' &&
75
+ !car.inPitLane &&
76
+ !trackContext?.originState?.inPitLane &&
77
+ Array.isArray(snapshot.track?.samples) &&
78
+ snapshot.track.samples.length > 0 &&
79
+ trackContext?.originState &&
80
+ canUseBatchTrainingRayApproximation(snapshot.track, trackContext.originState);
81
+ }
@@ -2,6 +2,7 @@ import { DEFAULT_RAY_ANGLES_DEGREES } from './rayDefaults.js';
2
2
  import { RAY_CHANNELS } from './rayChannels.js';
3
3
 
4
4
  const NORMALIZED_RAY_OPTIONS = Symbol('normalizedRayOptions');
5
+ const NORMALIZED_RAY_INPUTS = new WeakMap();
5
6
 
6
7
  export { RAY_CHANNELS } from './rayChannels.js';
7
8
 
@@ -63,9 +64,34 @@ export function normalizeRayOptions(rayOptions = {}) {
63
64
  value: true,
64
65
  enumerable: false,
65
66
  });
67
+ NORMALIZED_RAY_INPUTS.set(normalized, copyRayOptionsInput(rayOptions));
66
68
  return normalized;
67
69
  }
68
70
 
71
+ export function mergeRayOptionOverrides(baseRays = {}, overrides = {}) {
72
+ const baseInput = NORMALIZED_RAY_INPUTS.get(baseRays) ?? baseRays;
73
+ const overrideInput = NORMALIZED_RAY_INPUTS.get(overrides) ?? overrides;
74
+ const requested = Object.fromEntries(Object.entries(overrideInput).filter(([, value]) => value !== undefined));
75
+ const merged = { ...baseInput, ...requested };
76
+ if (Object.hasOwn(requested, 'lengthMeters') && !Object.hasOwn(requested, 'defaultLengthMeters')) {
77
+ delete merged.defaultLengthMeters;
78
+ }
79
+ if (Object.hasOwn(requested, 'rays')) return merged;
80
+ if (Object.hasOwn(requested, 'layout') || Object.hasOwn(requested, 'anglesDegrees')) {
81
+ delete merged.rays;
82
+ if (Object.hasOwn(requested, 'anglesDegrees') && !Object.hasOwn(requested, 'layout')) delete merged.layout;
83
+ }
84
+ return merged;
85
+ }
86
+
87
+ function copyRayOptionsInput(source) {
88
+ const input = { ...source };
89
+ if (Array.isArray(source.rays)) input.rays = source.rays.map((ray) => typeof ray === 'object' && ray ? { ...ray } : ray);
90
+ if (Array.isArray(source.anglesDegrees)) input.anglesDegrees = [...source.anglesDegrees];
91
+ if (Array.isArray(source.channels)) input.channels = [...source.channels];
92
+ return input;
93
+ }
94
+
69
95
  function normalizeRays(rayOptions, defaultLengthMeters) {
70
96
  const configuredRays = Array.isArray(rayOptions.rays)
71
97
  ? rayOptions.rays
@@ -20,10 +20,6 @@ export function pointOnRay(origin, ray, distance) {
20
20
  };
21
21
  }
22
22
 
23
- export function intersectAxisAlignedBoxRay(origin, ray, halfLength, halfWidth) {
24
- return intersectAxisAlignedBoxRayScalars(origin.x, origin.y, ray.x, ray.y, halfLength, halfWidth);
25
- }
26
-
27
23
  export function intersectAxisAlignedBoxRayScalars(originX, originY, rayX, rayY, halfLength, halfWidth) {
28
24
  let tMin = -Infinity;
29
25
  let tMax = Infinity;
@@ -50,10 +46,6 @@ export function intersectAxisAlignedBoxRayScalars(originX, originY, rayX, rayY,
50
46
  return Math.max(0, tMin);
51
47
  }
52
48
 
53
- export function dot(a, b) {
54
- return a.x * b.x + a.y * b.y;
55
- }
56
-
57
49
  export function degreesToRadians(degrees) {
58
50
  return (degrees * Math.PI) / 180;
59
51
  }
@@ -1,11 +1,5 @@
1
1
  import { metersToSimUnits } from '../../simulation/units.js';
2
2
 
3
- export function canUseLocalStripRayApproximation(track, state) {
4
- if (!track || !state || state.inPitLane) return false;
5
- const kerbOuter = track.width / 2 + (track.kerbWidth ?? 0);
6
- return Number.isFinite(state.crossTrackError) && state.crossTrackError <= kerbOuter;
7
- }
8
-
9
3
  export function canUseIndexedRecoveryRayApproximation(track, state) {
10
4
  if (!track || !state || state.inPitLane) return false;
11
5
  const recoveryBand = track.width / 2 +
@@ -0,0 +1,75 @@
1
+ import { createSurfaceMiss } from './surfaceRays.js';
2
+
3
+ export function writeRichRay(target, ray, angleRadians, roadEdge, carHit, surfaceHits, channels) {
4
+ target.id = ray.id;
5
+ target.angleDegrees = ray.angleDegrees;
6
+ target.angleRadians = angleRadians;
7
+ target.lengthMeters = ray.lengthMeters;
8
+ target.roadEdge = writeTrackRayHit(target.roadEdge ?? {}, roadEdge);
9
+ target.track = target.roadEdge;
10
+ target.kerb = channels.hasKerb
11
+ ? writeSurfaceRayHit(target.kerb ?? {}, surfaceHits.kerb ?? createSurfaceMiss(ray.lengthMeters))
12
+ : writeSurfaceRayHit(target.kerb ?? {}, createSurfaceMiss(ray.lengthMeters));
13
+ target.illegalSurface = channels.hasIllegalSurface
14
+ ? writeSurfaceRayHit(target.illegalSurface ?? {}, surfaceHits.illegalSurface ?? createSurfaceMiss(ray.lengthMeters))
15
+ : writeSurfaceRayHit(target.illegalSurface ?? {}, createSurfaceMiss(ray.lengthMeters));
16
+ target.car = writeCarRayHit(target.car ?? {}, carHit);
17
+ return target;
18
+ }
19
+
20
+ function writeTrackRayHit(target, source) {
21
+ target.hit = Boolean(source?.hit);
22
+ target.distanceMeters = source?.distanceMeters;
23
+ target.kind = source?.kind ?? null;
24
+ return target;
25
+ }
26
+
27
+ function writeSurfaceRayHit(target, source) {
28
+ target.hit = Boolean(source?.hit);
29
+ target.distanceMeters = source?.distanceMeters;
30
+ target.surface = source?.surface ?? null;
31
+ return target;
32
+ }
33
+
34
+ function writeCarRayHit(target, source) {
35
+ target.hit = Boolean(source?.hit);
36
+ target.distanceMeters = source?.distanceMeters;
37
+ target.driverId = source?.driverId ?? null;
38
+ target.targetId = source?.targetId ?? null;
39
+ target.targetType = source?.targetType ?? null;
40
+ target.relativeSpeedKph = source?.relativeSpeedKph ?? 0;
41
+ return target;
42
+ }
43
+
44
+ export function writeRayVectorValues(values, lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels) {
45
+ values.length = 0;
46
+ appendRayVectorValues(values, lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels);
47
+ return values;
48
+ }
49
+
50
+ export function appendRayVectorValues(values, lengthMeters, roadEdge, carHit, surfaceHits, surfaceChannels) {
51
+ values.push(
52
+ ratio(roadEdge.distanceMeters, lengthMeters),
53
+ roadEdge.hit ? 1 : 0,
54
+ roadEdge.kind === 'exit' ? 1 : 0,
55
+ roadEdge.kind === 'entry' ? 1 : 0,
56
+ ratio(carHit.distanceMeters, lengthMeters),
57
+ carHit.hit ? 1 : 0,
58
+ carHit.relativeSpeedKph / 200,
59
+ carHit.targetType === 'replayGhost' ? 1 : 0,
60
+ );
61
+ for (let index = 0; index < surfaceChannels.length; index += 1) {
62
+ const channel = surfaceChannels[index];
63
+ const hit = surfaceHits[channel] ?? createSurfaceMiss(lengthMeters);
64
+ values.push(
65
+ ratio(hit.distanceMeters, lengthMeters),
66
+ hit.hit ? 1 : 0,
67
+ );
68
+ }
69
+ return values;
70
+ }
71
+
72
+ function ratio(value, max) {
73
+ const finite = Number.isFinite(value) ? value : max;
74
+ return Math.max(0, Math.min(1, finite / Math.max(1e-9, max)));
75
+ }
@@ -0,0 +1,110 @@
1
+ import { estimateCarHit } from './carRays.js';
2
+ import { createTrackMiss, estimateTrackHit } from './trackRays.js';
3
+ import { estimateSurfaceHits } from './surfaceRays.js';
4
+ import { traceIndexedRayBands } from './rayBandTrace.js';
5
+ import { createRayChannelFlags, writeRayChannelFlags } from './rayChannels.js';
6
+ import { getCarRayStats, getRayBandTraceStats } from './rayScratch.js';
7
+ import { MODEL_RAY_SURFACE_CHANNELS } from '../observationSchema.js';
8
+ import { pitOverrideAllowedForCar } from '../../simulation/track/trackStatePolicy.js';
9
+
10
+ const CHANNEL_CONTEXT = Symbol('rayChannelContext');
11
+
12
+ export function estimateCarHitForRay(car, snapshot, ray, origin, vector, carTargets, sharedRayQuery, scratch) {
13
+ return estimateCarHit(car, snapshot, ray.angleDegrees, ray.lengthMeters, origin, carTargets, {
14
+ rayVector: vector,
15
+ resultTarget: sharedRayQuery?.carHitResult,
16
+ stats: getCarRayStats(scratch),
17
+ });
18
+ }
19
+
20
+ export function createRayChannelContext(normalized) {
21
+ if (normalized?.[CHANNEL_CONTEXT]) return normalized[CHANNEL_CONTEXT];
22
+ const channels = writeRayChannelFlags(createRayChannelFlags(), normalized.channels);
23
+ const context = {
24
+ channels,
25
+ surfaceChannels: MODEL_RAY_SURFACE_CHANNELS.filter((channel) => channels.has(channel)),
26
+ hasRoadEdge: channels.hasRoadEdge,
27
+ hasCar: channels.hasCar,
28
+ hasRoadOrSurface: channels.hasRoadEdge || channels.surfaceChannels.length > 0,
29
+ };
30
+ if (normalized) {
31
+ Object.defineProperty(normalized, CHANNEL_CONTEXT, {
32
+ value: context,
33
+ enumerable: false,
34
+ });
35
+ }
36
+ return context;
37
+ }
38
+
39
+ export function estimateRayBands({
40
+ car,
41
+ snapshot,
42
+ ray,
43
+ angleDegrees,
44
+ origin,
45
+ vector,
46
+ normalized,
47
+ channelContext,
48
+ trackContext,
49
+ sharedRayQuery,
50
+ scratch,
51
+ }) {
52
+ const stats = getRayBandTraceStats(scratch);
53
+ const canUseRayBandTrace = channelContext.hasRoadOrSurface &&
54
+ trackContext?.originState;
55
+ if (canUseRayBandTrace) {
56
+ const direct = traceIndexedRayBands({
57
+ track: snapshot.track,
58
+ origin,
59
+ vector,
60
+ lengthMeters: ray.lengthMeters,
61
+ originState: trackContext.originState,
62
+ channels: channelContext.channels,
63
+ precision: normalized.precision,
64
+ sharedRayQuery,
65
+ stats,
66
+ validateBoundaries: car?.interaction?.profile !== 'batch-training',
67
+ allowPitOverride: car?.interaction?.profile === 'batch-training'
68
+ ? false
69
+ : pitOverrideAllowedForCar(car),
70
+ mainTrackOnly: car?.interaction?.profile === 'batch-training',
71
+ });
72
+ if (direct.available) {
73
+ if (stats) {
74
+ if (direct.path === 'sampled') stats.sampledPathCount += 1;
75
+ else stats.directPathCount += 1;
76
+ }
77
+ return {
78
+ roadEdge: direct.roadEdge,
79
+ surfaceHits: {
80
+ kerb: direct.kerb,
81
+ illegalSurface: direct.illegalSurface,
82
+ },
83
+ };
84
+ }
85
+ if (stats) stats.fallbackCount += 1;
86
+ }
87
+
88
+ return {
89
+ roadEdge: channelContext.hasRoadEdge
90
+ ? estimateTrackHit(
91
+ car,
92
+ snapshot,
93
+ angleDegrees,
94
+ ray.lengthMeters,
95
+ trackContext,
96
+ { precision: normalized.precision, sharedRayQuery },
97
+ )
98
+ : createTrackMiss(ray.lengthMeters),
99
+ surfaceHits: estimateSurfaceHits(
100
+ car,
101
+ snapshot,
102
+ ray,
103
+ origin,
104
+ vector,
105
+ channelContext.surfaceChannels,
106
+ trackContext,
107
+ { precision: normalized.precision, sharedRayQuery },
108
+ ),
109
+ };
110
+ }
@@ -0,0 +1,146 @@
1
+ import { normalizeRayOptions } from './rayConfig.js';
2
+ import { getCarRayOrigin } from './rayGeometry.js';
3
+ import { isSelfCarTarget, rayDetectableTargetsForSnapshot } from './sensorTargets.js';
4
+
5
+ export function createRayBatchContext(snapshot, { scratch = false } = {}) {
6
+ const contextScratch = scratch === true
7
+ ? {}
8
+ : scratch && typeof scratch === 'object'
9
+ ? scratch
10
+ : null;
11
+ const context = {
12
+ rayTargets: rayDetectableTargetsForSnapshot(snapshot, contextScratch),
13
+ };
14
+ if (contextScratch) context.scratch = contextScratch;
15
+ return context;
16
+ }
17
+
18
+ export function getRayScratch(batchContext) {
19
+ return batchContext?.scratch ?? null;
20
+ }
21
+
22
+ export function getNormalizedRayOptions(rayOptions, scratch) {
23
+ if (!scratch || !rayOptions || typeof rayOptions !== 'object') return normalizeRayOptions(rayOptions);
24
+ if (rayOptions?.rays && rayOptions?.channels && rayOptions?.anglesDegrees) {
25
+ return normalizeRayOptions(rayOptions);
26
+ }
27
+ const cache = scratch.normalizedRayOptionsBySource ?? new WeakMap();
28
+ scratch.normalizedRayOptionsBySource = cache;
29
+ const cached = cache.get(rayOptions);
30
+ if (cached) return cached;
31
+ const normalized = normalizeRayOptions(rayOptions);
32
+ cache.set(rayOptions, normalized);
33
+ return normalized;
34
+ }
35
+
36
+ export function getRayOrigin(car, scratch) {
37
+ if (!scratch) return getCarRayOrigin(car);
38
+ const origin = scratch.origin ?? {};
39
+ scratch.origin = origin;
40
+ origin.x = car.x;
41
+ origin.y = car.y;
42
+ return origin;
43
+ }
44
+
45
+ export function getFilteredCarTargets(car, snapshot, batchContext, scratch) {
46
+ const targets = batchContext?.rayTargets ?? rayDetectableTargetsForSnapshot(snapshot);
47
+ if (!scratch) return targets.filter((target) => !isSelfCarTarget(car, target));
48
+ const filtered = scratch.carTargets ?? [];
49
+ scratch.carTargets = filtered;
50
+ let count = 0;
51
+ for (let index = 0; index < targets.length; index += 1) {
52
+ const target = targets[index];
53
+ if (isSelfCarTarget(car, target)) continue;
54
+ filtered[count] = target;
55
+ count += 1;
56
+ }
57
+ filtered.length = count;
58
+ return filtered;
59
+ }
60
+
61
+ export function prepareScratchArray(scratch, arrayKey, poolKey, count, createItem) {
62
+ if (!scratch) return null;
63
+ const values = scratch[arrayKey] ?? [];
64
+ const pool = scratch[poolKey] ?? [];
65
+ scratch[arrayKey] = values;
66
+ scratch[poolKey] = pool;
67
+ values.length = count;
68
+ for (let index = 0; index < count; index += 1) {
69
+ const item = pool[index] ?? createItem();
70
+ pool[index] = item;
71
+ values[index] = item;
72
+ }
73
+ return values;
74
+ }
75
+
76
+ export function getScratchRayVector(scratch, index, car, angleRadians) {
77
+ if (!scratch) {
78
+ return {
79
+ x: Math.cos((car.heading ?? 0) + angleRadians),
80
+ y: Math.sin((car.heading ?? 0) + angleRadians),
81
+ };
82
+ }
83
+ const vectors = scratch.rayVectors ?? [];
84
+ scratch.rayVectors = vectors;
85
+ const vector = vectors[index] ?? {};
86
+ vectors[index] = vector;
87
+ vector.x = Math.cos((car.heading ?? 0) + angleRadians);
88
+ vector.y = Math.sin((car.heading ?? 0) + angleRadians);
89
+ return vector;
90
+ }
91
+
92
+ export function getSharedRayQuery(scratch, index) {
93
+ if (!scratch) return {};
94
+ const queries = scratch.sharedRayQueries ?? [];
95
+ scratch.sharedRayQueries = queries;
96
+ const query = queries[index] ?? {};
97
+ queries[index] = query;
98
+ query.carHitResult = query.carHitResult ?? {};
99
+ query.surfaceBoundaries = null;
100
+ query.trackBandBoundaries = null;
101
+ query.boundaryStateCache = null;
102
+ query.sampleStateCache = null;
103
+ return query;
104
+ }
105
+
106
+ export function getCarRayStats(scratch) {
107
+ if (!scratch) return null;
108
+ const stats = scratch.carRayStats ?? {
109
+ callerVectorCount: 0,
110
+ computedVectorCount: 0,
111
+ resultTargetCount: 0,
112
+ };
113
+ scratch.carRayStats = stats;
114
+ return stats;
115
+ }
116
+
117
+ export function getRayBandTraceStats(scratch) {
118
+ if (!scratch) return null;
119
+ const stats = scratch.rayBandTraceStats ?? {
120
+ directPathCount: 0,
121
+ sampledPathCount: 0,
122
+ fallbackCount: 0,
123
+ channelSetAllocations: 0,
124
+ };
125
+ scratch.rayBandTraceStats = stats;
126
+ return stats;
127
+ }
128
+
129
+ export function clearRayBatchContextScratch(scratch) {
130
+ if (!scratch) return;
131
+ const arrayKeys = [
132
+ 'rayTargets',
133
+ 'rayTargetPool',
134
+ 'carTargets',
135
+ 'rayVectors',
136
+ 'sharedRayQueries',
137
+ 'rays',
138
+ 'rayPool',
139
+ 'rayVectorValues',
140
+ 'rayVectorValuePool',
141
+ ];
142
+ for (let index = 0; index < arrayKeys.length; index += 1) {
143
+ const value = scratch[arrayKeys[index]];
144
+ if (Array.isArray(value)) value.length = 0;
145
+ }
146
+ }
@@ -10,10 +10,6 @@ export function rayDetectableTargets(self, snapshot) {
10
10
  return rayDetectableTargetsForSnapshot(snapshot).filter((target) => !isSelfCarTarget(self, target));
11
11
  }
12
12
 
13
- export function nearbyDetectableTargets(self, snapshot) {
14
- return nearbyDetectableTargetsForSnapshot(snapshot).filter((target) => !isSelfCarTarget(self, target));
15
- }
16
-
17
13
  export function rayDetectableTargetsForSnapshot(snapshot, scratch = null) {
18
14
  if (scratch) {
19
15
  return writeRayDetectableTargetsForSnapshot(snapshot, scratch);
@@ -31,6 +27,18 @@ export function nearbyDetectableTargetsForSnapshot(snapshot) {
31
27
  ];
32
28
  }
33
29
 
30
+ // Shared only within one observation build, so mutable snapshots cannot leave
31
+ // stale positions, detectability flags, or replay entities in a later result.
32
+ export function createNearbyBatchContext(snapshot) {
33
+ const targetsById = new Map();
34
+ for (const car of snapshot.cars ?? []) targetsById.set(`car:${car.id}`, car);
35
+ for (const ghost of snapshot.replayGhosts ?? []) targetsById.set(`replayGhost:${ghost.id}`, ghost);
36
+ return {
37
+ nearbyTargets: nearbyDetectableTargetsForSnapshot(snapshot),
38
+ targetsById,
39
+ };
40
+ }
41
+
34
42
  function detectableCars(snapshot, predicate) {
35
43
  return (snapshot.cars ?? [])
36
44
  .filter((car) => predicate(car))