@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
@@ -2,31 +2,63 @@ import { clamp, normalizeAngle, wrapDistance } from '../simMath.js';
2
2
  import { PIT_LANE_WIDTH } from './trackConstants.js';
3
3
  import { pointInsideBounds } from './trackMath.js';
4
4
  import { nearestPitLaneState, resolveDirectConnectorPitLaneState } from './pitLaneState.js';
5
- import { queryNearestTrackProjection, queryNearestTrackProjectionInto } from './trackQueryIndex.js';
5
+ import { queryNearestTrackProjectionInto } from './trackQueryIndex.js';
6
6
  import { recordStat } from './trackQueryStats.js';
7
7
 
8
8
  export function pointAt(track, distanceAlong) {
9
+ return pointAtInto(track, distanceAlong, {});
10
+ }
11
+
12
+ export function pointAtInto(track, distanceAlong, target) {
9
13
  const wrapped = wrapDistance(distanceAlong, track.length);
10
- const low = sampleIndexAtDistance(track, wrapped);
14
+ const low = sampleIndexAtDistanceUnwrapped(track, wrapped);
11
15
 
12
16
  const next = track.samples[low] ?? track.samples[0];
13
17
  const previous = track.samples[Math.max(0, low - 1)] ?? next;
14
18
  const span = Math.max(1, next.distance - previous.distance);
15
19
  const amount = clamp((wrapped - previous.distance) / span, 0, 1);
16
20
 
17
- return {
18
- x: previous.x + (next.x - previous.x) * amount,
19
- y: previous.y + (next.y - previous.y) * amount,
20
- heading: previous.heading + normalizeAngle(next.heading - previous.heading) * amount,
21
- normalX: previous.normalX + (next.normalX - previous.normalX) * amount,
22
- normalY: previous.normalY + (next.normalY - previous.normalY) * amount,
23
- curvature: previous.curvature + (next.curvature - previous.curvature) * amount,
24
- distance: wrapped,
25
- };
21
+ target.x = previous.x + (next.x - previous.x) * amount;
22
+ target.y = previous.y + (next.y - previous.y) * amount;
23
+ target.heading = previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
24
+ target.normalX = previous.normalX + (next.normalX - previous.normalX) * amount;
25
+ target.normalY = previous.normalY + (next.normalY - previous.normalY) * amount;
26
+ target.curvature = previous.curvature + (next.curvature - previous.curvature) * amount;
27
+ target.distance = wrapped;
28
+ return target;
26
29
  }
27
30
 
28
- export function sampleIndexAtDistance(track, distanceAlong) {
31
+ export function sampleHeadingCurvatureAtInto(track, distanceAlong, target) {
29
32
  const wrapped = wrapDistance(distanceAlong, track.length);
33
+ const low = sampleIndexAtDistanceUnwrapped(track, wrapped);
34
+
35
+ const next = track.samples[low] ?? track.samples[0];
36
+ const previous = track.samples[Math.max(0, low - 1)] ?? next;
37
+ const span = Math.max(1, next.distance - previous.distance);
38
+ const amount = clamp((wrapped - previous.distance) / span, 0, 1);
39
+
40
+ target.heading = previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
41
+ target.curvature = previous.curvature + (next.curvature - previous.curvature) * amount;
42
+ return target;
43
+ }
44
+
45
+ export function sampleHeadingAt(track, distanceAlong) {
46
+ const wrapped = wrapDistance(distanceAlong, track.length);
47
+ const low = sampleIndexAtDistanceUnwrapped(track, wrapped);
48
+
49
+ const next = track.samples[low] ?? track.samples[0];
50
+ const previous = track.samples[Math.max(0, low - 1)] ?? next;
51
+ const span = Math.max(1, next.distance - previous.distance);
52
+ const amount = clamp((wrapped - previous.distance) / span, 0, 1);
53
+
54
+ return previous.heading + normalizeAngle(next.heading - previous.heading) * amount;
55
+ }
56
+
57
+ export function sampleIndexAtDistance(track, distanceAlong) {
58
+ return binarySampleIndexAtDistance(track, wrapDistance(distanceAlong, track.length));
59
+ }
60
+
61
+ function binarySampleIndexAtDistance(track, wrapped) {
30
62
  let low = 0;
31
63
  let high = track.samples.length - 1;
32
64
 
@@ -39,28 +71,41 @@ export function sampleIndexAtDistance(track, distanceAlong) {
39
71
  return low;
40
72
  }
41
73
 
42
- export function nearestSampleInRange(track, position, startIndex, endIndex) {
43
- const sampleCount = track.samples.length - 1;
44
- let best = null;
45
- let bestDistance = Infinity;
46
-
47
- for (let index = startIndex; index <= endIndex; index += 1) {
48
- const wrappedIndex = ((index % sampleCount) + sampleCount) % sampleCount;
49
- const sample = track.samples[wrappedIndex];
50
- const dx = position.x - sample.x;
51
- const dy = position.y - sample.y;
52
- const squared = dx * dx + dy * dy;
53
- if (squared < bestDistance) {
54
- bestDistance = squared;
55
- best = sample;
56
- }
74
+ // Uniform-bin index over sample distances: table[bin] holds the smallest
75
+ // sample index whose distance reaches the bin start, so a lookup plus a short
76
+ // forward walk returns exactly what the binary search would, in O(1). Built
77
+ // eagerly when the track query index is attached; lookups on sample arrays
78
+ // without a prepared table fall back to the binary search.
79
+ const SAMPLE_INDEX_LUTS = new WeakMap();
80
+
81
+ export function prepareSampleIndexLut(track) {
82
+ const samples = track?.samples;
83
+ if (!Array.isArray(samples) || samples.length < 2 || !Number.isFinite(track.length) || track.length <= 0) return;
84
+ if (SAMPLE_INDEX_LUTS.has(samples)) return;
85
+ const lastIndex = samples.length - 1;
86
+ const bins = Math.max(1, lastIndex * 2);
87
+ const table = new Int32Array(bins);
88
+ const binSize = track.length / bins;
89
+ let index = 0;
90
+ for (let bin = 0; bin < bins; bin += 1) {
91
+ const start = bin * binSize;
92
+ while (index < lastIndex && samples[index].distance < start) index += 1;
93
+ table[bin] = index;
57
94
  }
58
-
59
- return { best, bestDistance };
95
+ SAMPLE_INDEX_LUTS.set(samples, { bins, binSize, table });
60
96
  }
61
97
 
62
- export function nearestSampleGlobal(track, position) {
63
- return nearestSampleInRange(track, position, 0, track.samples.length - 2);
98
+ function sampleIndexAtDistanceUnwrapped(track, wrapped) {
99
+ const samples = track.samples;
100
+ const lut = Number.isFinite(wrapped) ? SAMPLE_INDEX_LUTS.get(samples) : null;
101
+ if (!lut) return binarySampleIndexAtDistance(track, wrapped);
102
+ const lastIndex = samples.length - 1;
103
+ let bin = Math.floor(wrapped / lut.binSize);
104
+ if (bin < 0) bin = 0;
105
+ else if (bin >= lut.bins) bin = lut.bins - 1;
106
+ let index = lut.table[bin];
107
+ while (index < lastIndex && samples[index].distance < wrapped) index += 1;
108
+ return index;
64
109
  }
65
110
 
66
111
  export function createTrackState(track, position, best) {
@@ -0,0 +1,54 @@
1
+ import { CENTERLINE_CONTROLS, TRACK } from './trackConstants.js';
2
+ import { distance } from './trackMath.js';
3
+ import { rawCenterPoint } from './centerlineSpline.js';
4
+ import { recalculateSampleGeometry } from './sampleGeometry.js';
5
+ import { rotateSamplesToStandingStart, straightenStandingStartSamples } from './startStraight.js';
6
+ import { prepareSampleIndexLut } from './spatialQueries.js';
7
+ import { createTrackSectors } from './trackSectors.js';
8
+ import { deriveDrsZones, normalizeDrsZone } from './drsZones.js';
9
+ import { createPitLaneModel } from './pitLaneLayout.js';
10
+ import { attachTrackQueryIndex, createTrackQueryIndex } from './trackQueryConstruction.js';
11
+ import { attachTrackSnapshotJsonSerializer } from './trackSnapshotJson.js';
12
+ import { cacheTrackModel, getCachedTrackModel } from './trackModelCache.js';
13
+
14
+ export function buildTrackModel(track = TRACK) {
15
+ const cached = getCachedTrackModel(track);
16
+ if (cached) return cached;
17
+
18
+ const controls = track.centerlineControls ?? CENTERLINE_CONTROLS;
19
+ const base = [];
20
+ for (let index = 0; index <= track.sampleCount; index += 1) {
21
+ base.push(rawCenterPoint(index / track.sampleCount, controls, track.curveInterpolation));
22
+ }
23
+
24
+ let totalLength = 0;
25
+ const samples = base.map((point, index) => {
26
+ if (index > 0) totalLength += distance(base[index - 1], point);
27
+ return { ...point, distance: totalLength, heading: 0, normalX: 0, normalY: 0, curvature: 0 };
28
+ });
29
+ recalculateSampleGeometry(samples);
30
+
31
+ const startStraightOptions = track.generationOptions?.startStraight;
32
+ const rotatedSamples = rotateSamplesToStandingStart(samples, totalLength, startStraightOptions);
33
+ const straightenedTrack = straightenStandingStartSamples(rotatedSamples, totalLength, startStraightOptions);
34
+ const normalizedSamples = straightenedTrack.samples;
35
+ totalLength = straightenedTrack.totalLength;
36
+
37
+ const model = {
38
+ ...track,
39
+ centerlineControls: controls,
40
+ length: totalLength,
41
+ samples: normalizedSamples,
42
+ sectors: createTrackSectors(totalLength),
43
+ drsZones: (track.drsZones ?? deriveDrsZones(normalizedSamples, totalLength))
44
+ .map((zone) => normalizeDrsZone(zone, totalLength)),
45
+ };
46
+ model.pitLane = track.pitLane?.enabled === false || track.generationOptions?.pitLane?.enabled === false
47
+ ? null
48
+ : createPitLaneModel(model);
49
+ attachTrackQueryIndex(model, createTrackQueryIndex(model));
50
+ prepareSampleIndexLut(model);
51
+ attachTrackSnapshotJsonSerializer(model);
52
+ cacheTrackModel(track, model);
53
+ return model;
54
+ }
@@ -34,7 +34,6 @@ export const START_STRAIGHT_GRID_LENGTH = metersToSimUnits(496);
34
34
  export const START_STRAIGHT_EXIT_LENGTH = metersToSimUnits(500);
35
35
  export const START_STRAIGHT_LOCK_EXTRA = metersToSimUnits(43);
36
36
  export const START_STRAIGHT_BLEND_LENGTH = metersToSimUnits(320);
37
- export const NEAREST_HINT_WINDOW_SAMPLES = 240;
38
37
  export const PIT_LANE_WIDTH = metersToSimUnits(12);
39
38
  export const PIT_LANE_EDGE_GAP = metersToSimUnits(16);
40
39
  export const PIT_ACCESS_MIN_LENGTH = metersToSimUnits(70);
@@ -66,27 +65,6 @@ export const PIT_LANE_FINISH_RATIO = 0.64;
66
65
  export const PIT_WORLD_PADDING = metersToSimUnits(24);
67
66
  export const PIT_TRACK_CLEARANCE_MARGIN = metersToSimUnits(5);
68
67
  export const PIT_LANE_OFFSET_SEARCH_STEP = metersToSimUnits(12);
69
- export const PROCEDURAL_TRACK_TEMPLATES = [
70
- [
71
- [0.08, 0.55], [0.10, 0.80], [0.22, 0.89], [0.42, 0.84], [0.52, 0.93],
72
- [0.60, 0.75], [0.74, 0.88], [0.91, 0.76], [0.94, 0.54], [0.82, 0.46],
73
- [0.94, 0.28], [0.78, 0.18], [0.62, 0.31], [0.54, 0.13], [0.43, 0.30],
74
- [0.33, 0.17], [0.20, 0.24], [0.12, 0.38], [0.22, 0.48], [0.13, 0.50],
75
- ],
76
- [
77
- [0.07, 0.46], [0.14, 0.72], [0.25, 0.83], [0.39, 0.73], [0.47, 0.88],
78
- [0.56, 0.70], [0.67, 0.82], [0.88, 0.84], [0.95, 0.63], [0.83, 0.56],
79
- [0.92, 0.43], [0.79, 0.35], [0.88, 0.20], [0.68, 0.15], [0.58, 0.29],
80
- [0.47, 0.18], [0.34, 0.30], [0.21, 0.19], [0.10, 0.28], [0.17, 0.39],
81
- ],
82
- [
83
- [0.06, 0.61], [0.13, 0.86], [0.31, 0.91], [0.43, 0.79], [0.57, 0.87],
84
- [0.71, 0.70], [0.92, 0.72], [0.95, 0.50], [0.84, 0.43], [0.91, 0.32],
85
- [0.74, 0.24], [0.69, 0.11], [0.54, 0.18], [0.45, 0.08], [0.35, 0.22],
86
- [0.23, 0.17], [0.11, 0.31], [0.24, 0.43], [0.15, 0.52], [0.28, 0.60],
87
- ],
88
- ];
89
-
90
68
  export const CENTERLINE_CONTROLS = [
91
69
  { x: WORLD.width * 0.05, y: WORLD.height * 0.56 },
92
70
  { x: WORLD.width * 0.10, y: WORLD.height * 0.81 },
@@ -161,35 +161,6 @@ export function pointInsideBounds(point, bounds) {
161
161
  );
162
162
  }
163
163
 
164
- export function projectPointToSegment(position, start, end) {
165
- const dx = end.x - start.x;
166
- const dy = end.y - start.y;
167
- const lengthSquared = dx * dx + dy * dy;
168
- const amount = lengthSquared > 0
169
- ? clamp(((position.x - start.x) * dx + (position.y - start.y) * dy) / lengthSquared, 0, 1)
170
- : 0;
171
- const projected = {
172
- x: start.x + dx * amount,
173
- y: start.y + dy * amount,
174
- };
175
- const length = Math.sqrt(lengthSquared);
176
- const heading = Math.atan2(dy, dx);
177
- const normalX = length > 0 ? -dy / length : 0;
178
- const normalY = length > 0 ? dx / length : 1;
179
- const signedOffset = (position.x - projected.x) * normalX + (position.y - projected.y) * normalY;
180
-
181
- return {
182
- point: projected,
183
- amount,
184
- length,
185
- heading,
186
- normalX,
187
- normalY,
188
- signedOffset,
189
- crossTrackError: Math.abs(signedOffset),
190
- };
191
- }
192
-
193
164
  export function nearestPointOnPolyline(points, position, segmentIndices = null) {
194
165
  return nearestPointOnPolylineInto({}, points, position, { segmentIndices });
195
166
  }
@@ -306,3 +277,7 @@ export function pointIsInsidePolygon(point, polygon) {
306
277
 
307
278
  return inside;
308
279
  }
280
+
281
+ export function finitePoint(point) {
282
+ return Number.isFinite(point?.x) && Number.isFinite(point?.y);
283
+ }
@@ -1,242 +1,14 @@
1
- import { normalizeAngle } from '../simMath.js';
2
- import { CENTERLINE_CONTROLS, TRACK } from './trackConstants.js';
3
- import { distance } from './trackMath.js';
4
- import { rawCenterPoint } from './centerlineSpline.js';
5
- import { generateCenterlineControls, generateFallbackCenterlineControls, generateSafeFallbackCenterlineControls, normalizeSeed } from './proceduralCenterline.js';
6
- import { isValidProceduralTrackModel } from './trackValidation.js';
7
- import { recalculateSampleGeometry } from './sampleGeometry.js';
8
- import { rotateSamplesToStandingStart, straightenStandingStartSamples } from './startStraight.js';
9
- import { createTrackSectors } from './trackSectors.js';
10
- import { deriveDrsZones, normalizeDrsZone } from './drsZones.js';
11
- import { createPitLaneModel } from './pitLaneLayout.js';
12
- import { attachTrackQueryIndex, createTrackQueryIndex } from './trackQueryIndex.js';
13
- import { resolveProceduralTrackOptions } from './trackGenerationOptions.js';
14
- import { attachTrackSnapshotJsonSerializer } from './trackSnapshotJson.js';
15
-
16
1
  export { WORLD, TRACK } from './trackConstants.js';
2
+ export { normalizeAngle } from '../simMath.js';
17
3
  export { isInDrsZone } from './drsZones.js';
18
- export { nearestTrackState, offsetTrackPoint, pointAt } from './spatialQueries.js';
19
-
20
- const PROCEDURAL_TRACK_CACHE = new Map();
21
- const TRACK_MODEL_CACHE = new WeakMap();
22
-
23
- export function buildTrackModel(track = TRACK) {
24
- const canReuseCachedModel = track === TRACK || Object.isFrozen(track);
25
- const cached = canReuseCachedModel ? TRACK_MODEL_CACHE.get(track) : null;
26
- if (cached) return cached;
27
-
28
- const controls = track.centerlineControls ?? CENTERLINE_CONTROLS;
29
- const base = [];
30
- for (let index = 0; index <= track.sampleCount; index += 1) {
31
- base.push(rawCenterPoint(index / track.sampleCount, controls, track.curveInterpolation));
32
- }
33
-
34
- let totalLength = 0;
35
- const samples = base.map((point, index) => {
36
- if (index > 0) totalLength += distance(base[index - 1], point);
37
- return { ...point, distance: totalLength, heading: 0, normalX: 0, normalY: 0, curvature: 0 };
38
- });
39
- recalculateSampleGeometry(samples);
40
-
41
- const startStraightOptions = track.generationOptions?.startStraight;
42
- const rotatedSamples = rotateSamplesToStandingStart(samples, totalLength, startStraightOptions);
43
- const straightenedTrack = straightenStandingStartSamples(rotatedSamples, totalLength, startStraightOptions);
44
- const normalizedSamples = straightenedTrack.samples;
45
- totalLength = straightenedTrack.totalLength;
46
-
47
- const model = {
48
- ...track,
49
- centerlineControls: controls,
50
- length: totalLength,
51
- samples: normalizedSamples,
52
- sectors: createTrackSectors(totalLength),
53
- drsZones: (track.drsZones ?? deriveDrsZones(normalizedSamples, totalLength))
54
- .map((zone) => normalizeDrsZone(zone, totalLength)),
55
- };
56
- model.pitLane = track.pitLane?.enabled === false || track.generationOptions?.pitLane?.enabled === false
57
- ? null
58
- : createPitLaneModel(model);
59
- attachTrackQueryIndex(model, createTrackQueryIndex(model));
60
- attachTrackSnapshotJsonSerializer(model);
61
- if (canReuseCachedModel) {
62
- TRACK_MODEL_CACHE.set(track, freezeTrackModel(model));
63
- }
64
- return model;
65
- }
66
-
67
- export function createProceduralTrack(seed = Date.now(), options = {}) {
68
- const normalizedSeed = normalizeSeed(seed);
69
- const generationOptions = resolveProceduralTrackOptions(options);
70
- const cacheKey = `${normalizedSeed}:${generationOptions.cacheKey}`;
71
- const cached = PROCEDURAL_TRACK_CACHE.get(cacheKey);
72
- if (cached) return cached;
73
-
74
- for (let attempt = 0; attempt < generationOptions.attempts.primary; attempt += 1) {
75
- const candidateSeed = (normalizedSeed + Math.imul(attempt, 2654435761)) >>> 0;
76
- const candidate = {
77
- ...TRACK,
78
- name: `Generated GP ${candidateSeed.toString(36).toUpperCase().padStart(6, '0').slice(-6)}`,
79
- seed: candidateSeed,
80
- curveInterpolation: 'centripetal',
81
- centerlineControls: generateCenterlineControls(candidateSeed, generationOptions.shape),
82
- drsZones: null,
83
- generationOptions,
84
- ...(generationOptions.pitLane.enabled === false ? { pitLane: { enabled: false } } : {}),
85
- };
86
- const model = buildTrackModel(candidate);
87
- if (isValidProceduralTrackModel(model, generationOptions)) {
88
- const trackDefinition = {
89
- ...candidate,
90
- drsZones: model.drsZones.map(({ id, startRatio, endRatio }) => ({ id, startRatio, endRatio })),
91
- };
92
- return cacheProceduralTrackDefinition(cacheKey, trackDefinition);
93
- }
94
- }
95
-
96
- let fallback = null;
97
- let fallbackModel = null;
98
- for (let attempt = 0; attempt < generationOptions.attempts.fallback; attempt += 1) {
99
- const candidateSeed = (normalizedSeed ^ Math.imul(attempt + 1, 2246822519)) >>> 0;
100
- const candidate = {
101
- ...TRACK,
102
- name: `Generated GP ${normalizedSeed.toString(36).toUpperCase().padStart(6, '0').slice(-6)}`,
103
- seed: candidateSeed,
104
- curveInterpolation: 'centripetal',
105
- centerlineControls: generateFallbackCenterlineControls(candidateSeed, generationOptions.shape),
106
- drsZones: null,
107
- generationOptions,
108
- ...(generationOptions.pitLane.enabled === false ? { pitLane: { enabled: false } } : {}),
109
- };
110
- const model = buildTrackModel(candidate);
111
- if (isValidProceduralTrackModel(model, generationOptions)) {
112
- fallback = candidate;
113
- fallbackModel = model;
114
- break;
115
- }
116
- }
117
-
118
- if (!fallback || !fallbackModel) {
119
- fallback = {
120
- ...TRACK,
121
- name: `Generated GP ${normalizedSeed.toString(36).toUpperCase().padStart(6, '0').slice(-6)}`,
122
- seed: normalizedSeed,
123
- curveInterpolation: 'centripetal',
124
- centerlineControls: generateSafeFallbackCenterlineControls(normalizedSeed, generationOptions.shape),
125
- drsZones: null,
126
- generationOptions,
127
- ...(generationOptions.pitLane.enabled === false ? { pitLane: { enabled: false } } : {}),
128
- };
129
- fallbackModel = buildTrackModel(fallback);
130
- }
131
- const fallbackDefinition = {
132
- ...fallback,
133
- drsZones: fallbackModel.drsZones.map(({ id, startRatio, endRatio }) => ({ id, startRatio, endRatio })),
134
- };
135
- return cacheProceduralTrackDefinition(cacheKey, fallbackDefinition);
136
- }
137
-
138
- export { normalizeAngle };
139
-
140
- function cacheProceduralTrackDefinition(cacheKey, trackDefinition) {
141
- const frozen = deepFreeze(trackDefinition);
142
- PROCEDURAL_TRACK_CACHE.set(cacheKey, frozen);
143
- return frozen;
144
- }
145
-
146
- function freezeTrackModel(model) {
147
- freezeArrayItems(model.samples);
148
- freezeArrayItems(model.centerlineControls);
149
- freezeArrayItems(model.sectors);
150
- freezeArrayItems(model.drsZones);
151
- deepFreeze(model.pitLane);
152
- freezeTrackQueryIndex(model.queryIndex);
153
- return Object.freeze(model);
154
- }
155
-
156
- function freezeArrayItems(items) {
157
- if (!Array.isArray(items)) return;
158
- items.forEach((item) => deepFreeze(item));
159
- Object.freeze(items);
160
- }
161
-
162
- function deepFreeze(value) {
163
- if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
164
- Object.freeze(value);
165
- Object.values(value).forEach((child) => {
166
- deepFreeze(child);
167
- });
168
- return value;
169
- }
170
-
171
- function freezeTrackQueryIndex(index) {
172
- if (!index || typeof index !== 'object') return index;
173
- deepFreeze(index.bands);
174
- freezeCenterlineSegments(index.centerline);
175
- freezeSpatialGrid(index.grid);
176
- freezeSpatialGrid(index.segmentGrid);
177
- freezeArcBuckets(index.arcBuckets);
178
- freezePitQueryIndex(index.pit);
179
- return Object.freeze(index);
180
- }
181
-
182
- function freezeCenterlineSegments(centerline) {
183
- if (!centerline || typeof centerline !== 'object') return centerline;
184
- Object.entries(centerline).forEach(([key, value]) => {
185
- if (ArrayBuffer.isView(value)) {
186
- centerline[key] = Object.freeze(Array.from(value));
187
- }
188
- });
189
- return Object.freeze(centerline);
190
- }
191
-
192
- function freezeSpatialGrid(grid) {
193
- if (!grid || typeof grid !== 'object') return grid;
194
- deepFreeze(grid.bounds);
195
- if (grid.cells instanceof Map) {
196
- for (const cell of grid.cells.values()) Object.freeze(cell);
197
- grid.cells = readonlyMap(grid.cells);
198
- }
199
- return Object.freeze(grid);
200
- }
201
-
202
- function freezeArcBuckets(arcBuckets) {
203
- if (!arcBuckets || typeof arcBuckets !== 'object') return arcBuckets;
204
- if (Array.isArray(arcBuckets.buckets)) {
205
- arcBuckets.buckets.forEach((bucket) => Object.freeze(bucket));
206
- Object.freeze(arcBuckets.buckets);
207
- }
208
- return Object.freeze(arcBuckets);
209
- }
210
-
211
- function freezePitQueryIndex(pit) {
212
- if (!pit || typeof pit !== 'object') return pit;
213
- freezeSpatialGrid(pit.roadGrid);
214
- freezeSpatialGrid(pit.boxGrid);
215
- deepFreeze(pit.routes);
216
- freezeArrayItems(pit.roadSegments);
217
- freezeArrayItems(pit.boxCandidates);
218
- return Object.freeze(pit);
219
- }
220
-
221
- function readonlyMap(map) {
222
- return new Proxy(map, {
223
- get(target, property) {
224
- if (property === 'set' || property === 'delete' || property === 'clear') {
225
- return () => {
226
- throw new TypeError('Cannot mutate a cached track query index');
227
- };
228
- }
229
- const value = Reflect.get(target, property, target);
230
- return typeof value === 'function' ? value.bind(target) : value;
231
- },
232
- set() {
233
- throw new TypeError('Cannot mutate a cached track query index');
234
- },
235
- deleteProperty() {
236
- throw new TypeError('Cannot mutate a cached track query index');
237
- },
238
- defineProperty() {
239
- throw new TypeError('Cannot mutate a cached track query index');
240
- },
241
- });
242
- }
4
+ export { buildTrackModel } from './trackBuilder.js';
5
+ export { createProceduralTrack } from './proceduralTrack.js';
6
+ export { PROCEDURAL_TRACK_CACHE_MAX_ENTRIES } from './trackModelCache.js';
7
+ export {
8
+ nearestTrackState,
9
+ offsetTrackPoint,
10
+ pointAt,
11
+ pointAtInto,
12
+ sampleHeadingAt,
13
+ sampleHeadingCurvatureAtInto,
14
+ } from './spatialQueries.js';
@@ -0,0 +1,105 @@
1
+ import { TRACK } from './trackConstants.js';
2
+
3
+ const PROCEDURAL_TRACK_CACHE = new Map();
4
+ const TRACK_MODEL_CACHE = new WeakMap();
5
+ export const PROCEDURAL_TRACK_CACHE_MAX_ENTRIES = 8;
6
+
7
+ export function getCachedTrackModel(track) {
8
+ return track === TRACK || Object.isFrozen(track) ? TRACK_MODEL_CACHE.get(track) : null;
9
+ }
10
+
11
+ export function cacheTrackModel(track, model) {
12
+ if (track === TRACK || Object.isFrozen(track)) TRACK_MODEL_CACHE.set(track, freezeTrackModel(model));
13
+ }
14
+
15
+ export function getCachedProceduralTrack(cacheKey) {
16
+ const cached = PROCEDURAL_TRACK_CACHE.get(cacheKey);
17
+ if (!cached) return null;
18
+ PROCEDURAL_TRACK_CACHE.delete(cacheKey);
19
+ PROCEDURAL_TRACK_CACHE.set(cacheKey, cached);
20
+ return cached;
21
+ }
22
+
23
+ export function cacheProceduralTrackDefinition(cacheKey, trackDefinition, model) {
24
+ const frozen = deepFreeze(trackDefinition);
25
+ if (model) TRACK_MODEL_CACHE.set(frozen, freezeTrackModel(model));
26
+ while (PROCEDURAL_TRACK_CACHE.size >= PROCEDURAL_TRACK_CACHE_MAX_ENTRIES) {
27
+ const oldestKey = PROCEDURAL_TRACK_CACHE.keys().next().value;
28
+ if (oldestKey == null) break;
29
+ PROCEDURAL_TRACK_CACHE.delete(oldestKey);
30
+ }
31
+ PROCEDURAL_TRACK_CACHE.set(cacheKey, frozen);
32
+ return frozen;
33
+ }
34
+
35
+ function freezeTrackModel(model) {
36
+ freezeArrayItems(model.samples);
37
+ freezeArrayItems(model.centerlineControls);
38
+ freezeArrayItems(model.sectors);
39
+ freezeArrayItems(model.drsZones);
40
+ deepFreeze(model.pitLane);
41
+ freezeTrackQueryIndex(model.queryIndex);
42
+ return Object.freeze(model);
43
+ }
44
+
45
+ function freezeArrayItems(items) {
46
+ if (!Array.isArray(items)) return;
47
+ items.forEach((item) => deepFreeze(item));
48
+ Object.freeze(items);
49
+ }
50
+
51
+ function deepFreeze(value) {
52
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
53
+ Object.freeze(value);
54
+ Object.values(value).forEach((child) => {
55
+ deepFreeze(child);
56
+ });
57
+ return value;
58
+ }
59
+
60
+ function freezeTrackQueryIndex(index) {
61
+ if (!index || typeof index !== 'object') return index;
62
+ deepFreeze(index.bands);
63
+ freezeCenterlineSegments(index.centerline);
64
+ freezeSpatialGrid(index.grid);
65
+ freezeSpatialGrid(index.segmentGrid);
66
+ freezeArcBuckets(index.arcBuckets);
67
+ freezePitQueryIndex(index.pit);
68
+ return Object.freeze(index);
69
+ }
70
+
71
+ function freezeCenterlineSegments(centerline) {
72
+ if (!centerline || typeof centerline !== 'object') return centerline;
73
+ Object.entries(centerline).forEach(([key, value]) => {
74
+ if (ArrayBuffer.isView(value)) {
75
+ centerline[key] = Object.freeze(Array.from(value));
76
+ }
77
+ });
78
+ return Object.freeze(centerline);
79
+ }
80
+
81
+ function freezeSpatialGrid(grid) {
82
+ if (!grid || typeof grid !== 'object') return grid;
83
+ deepFreeze(grid.bounds);
84
+ freezeArrayItems(grid.cells);
85
+ return Object.freeze(grid);
86
+ }
87
+
88
+ function freezeArcBuckets(arcBuckets) {
89
+ if (!arcBuckets || typeof arcBuckets !== 'object') return arcBuckets;
90
+ if (Array.isArray(arcBuckets.buckets)) {
91
+ arcBuckets.buckets.forEach((bucket) => Object.freeze(bucket));
92
+ Object.freeze(arcBuckets.buckets);
93
+ }
94
+ return Object.freeze(arcBuckets);
95
+ }
96
+
97
+ function freezePitQueryIndex(pit) {
98
+ if (!pit || typeof pit !== 'object') return pit;
99
+ freezeSpatialGrid(pit.roadGrid);
100
+ freezeSpatialGrid(pit.boxGrid);
101
+ deepFreeze(pit.routes);
102
+ freezeArrayItems(pit.roadSegments);
103
+ freezeArrayItems(pit.boxCandidates);
104
+ return Object.freeze(pit);
105
+ }