@inventure71/paddockjs 0.3.0 → 2.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 (202) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/INSTALL_AND_UPDATE.md +5 -2
  3. package/README.md +328 -13
  4. package/docs/architecture.md +150 -41
  5. package/docs/concepts.md +22 -3
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +474 -34
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +1 -0
  10. package/docs/learnings.md +23 -1
  11. package/docs/rules.md +169 -21
  12. package/docs/system_specs.md +56 -20
  13. package/docs/training.md +369 -25
  14. package/examples/python/README.md +62 -0
  15. package/examples/python/base_policy_server.py +191 -0
  16. package/examples/python/requirements.txt +2 -0
  17. package/examples/train-basic-policy.mjs +5 -6
  18. package/examples/trainingData.mjs +133 -0
  19. package/package.json +15 -4
  20. package/src/api/PaddockSimulatorController.js +36 -2
  21. package/src/app/BrowserExpertAdapter.js +367 -9
  22. package/src/app/F1SimulatorApp.js +615 -940
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +14 -1
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +24 -4
  48. package/src/environment/actions.js +38 -7
  49. package/src/environment/controllerLoop.js +246 -0
  50. package/src/environment/episode.js +86 -2
  51. package/src/environment/evaluation.js +206 -0
  52. package/src/environment/index.d.ts +638 -6
  53. package/src/environment/index.js +14 -0
  54. package/src/environment/metrics.js +159 -0
  55. package/src/environment/observationOptions.js +33 -0
  56. package/src/environment/observations.js +500 -43
  57. package/src/environment/options.js +41 -2
  58. package/src/environment/recorder.js +28 -0
  59. package/src/environment/runtime.js +260 -24
  60. package/src/environment/scenarios.js +249 -0
  61. package/src/environment/sensors/bodySenses.js +23 -0
  62. package/src/environment/sensors/boundarySenses.js +17 -0
  63. package/src/environment/sensors/carRays.js +75 -0
  64. package/src/environment/sensors/contactSenses.js +32 -0
  65. package/src/environment/sensors/index.js +157 -0
  66. package/src/environment/sensors/indexedRayBands.js +103 -0
  67. package/src/environment/sensors/nearbyCars.js +52 -0
  68. package/src/environment/sensors/opponentRadar.js +65 -0
  69. package/src/environment/sensors/rayConfig.js +110 -0
  70. package/src/environment/sensors/rayDefaults.js +6 -0
  71. package/src/environment/sensors/rayGeometry.js +60 -0
  72. package/src/environment/sensors/rayGuards.js +19 -0
  73. package/src/environment/sensors/sensorTargets.js +60 -0
  74. package/src/environment/sensors/surfaceRays.js +296 -0
  75. package/src/environment/sensors/trackRays.js +261 -0
  76. package/src/environment/sensors.js +1 -257
  77. package/src/environment/specs.js +188 -9
  78. package/src/environment/workerProtocol.js +64 -0
  79. package/src/index.d.ts +533 -7
  80. package/src/index.js +26 -0
  81. package/src/rendering/proceduralTrackAsset.js +20 -510
  82. package/src/rendering/renderSnapshot.js +38 -12
  83. package/src/rendering/track/finishLineRenderer.js +34 -0
  84. package/src/rendering/track/gridRenderer.js +47 -0
  85. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  86. package/src/rendering/track/pitLaneRenderer.js +248 -0
  87. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  88. package/src/rendering/track/trackRenderConstants.js +38 -0
  89. package/src/rendering/track/trackRenderGeometry.js +94 -0
  90. package/src/simulation/collisionGeometry.js +214 -0
  91. package/src/simulation/driver/driverControlConstants.js +28 -0
  92. package/src/simulation/driver/driverController.js +47 -0
  93. package/src/simulation/driver/driverInput.js +28 -0
  94. package/src/simulation/driver/driverMath.js +6 -0
  95. package/src/simulation/driver/driverPersonality.js +25 -0
  96. package/src/simulation/driver/edgeRecovery.js +48 -0
  97. package/src/simulation/driver/racingControls.js +399 -0
  98. package/src/simulation/driver/racingLinePlan.js +166 -0
  99. package/src/simulation/driver/recoveryDynamics.js +38 -0
  100. package/src/simulation/driver/rejoinControls.js +213 -0
  101. package/src/simulation/driver/safetyCarControls.js +35 -0
  102. package/src/simulation/driver/trafficScan.js +51 -0
  103. package/src/simulation/driverController.js +4 -253
  104. package/src/simulation/participants/participantInteractions.js +120 -0
  105. package/src/simulation/pit/pitFlow.js +234 -0
  106. package/src/simulation/pit/pitIntent.js +92 -0
  107. package/src/simulation/pit/pitOccupancy.js +47 -0
  108. package/src/simulation/pit/pitPenaltyService.js +71 -0
  109. package/src/simulation/pit/pitQueue.js +55 -0
  110. package/src/simulation/pit/pitRouteMovement.js +160 -0
  111. package/src/simulation/pit/pitRouting.js +319 -0
  112. package/src/simulation/pit/pitService.js +41 -0
  113. package/src/simulation/pit/pitServiceConstants.js +21 -0
  114. package/src/simulation/pit/pitServiceProfile.js +45 -0
  115. package/src/simulation/pit/pitSnapshots.js +61 -0
  116. package/src/simulation/pit/pitState.js +113 -0
  117. package/src/simulation/pit/pitTireService.js +92 -0
  118. package/src/simulation/race/classification.js +104 -0
  119. package/src/simulation/race/gridPenalties.js +37 -0
  120. package/src/simulation/race/raceControlState.js +38 -0
  121. package/src/simulation/race/raceDistance.js +32 -0
  122. package/src/simulation/race/raceFinish.js +65 -0
  123. package/src/simulation/race/raceLifecycle.js +71 -0
  124. package/src/simulation/race/raceOrder.js +113 -0
  125. package/src/simulation/race/raceProgress.js +124 -0
  126. package/src/simulation/race/raceSetup.js +93 -0
  127. package/src/simulation/race/raceStep.js +80 -0
  128. package/src/simulation/race/redFlag.js +15 -0
  129. package/src/simulation/race/retirements.js +44 -0
  130. package/src/simulation/race/safetyCar.js +86 -0
  131. package/src/simulation/race/stalledDnf.js +86 -0
  132. package/src/simulation/raceSimulation.js +386 -1012
  133. package/src/simulation/replay/replayGhosts.js +128 -0
  134. package/src/simulation/rules/collisionSteward.js +69 -0
  135. package/src/simulation/rules/moduleConfig.js +72 -0
  136. package/src/simulation/rules/penaltyConfig.js +99 -0
  137. package/src/simulation/rules/penaltyEvents.js +18 -0
  138. package/src/simulation/rules/penaltyLedger.js +153 -0
  139. package/src/simulation/rules/penaltyStats.js +26 -0
  140. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  141. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  142. package/src/simulation/rules/ruleDefaults.js +85 -0
  143. package/src/simulation/rules/rulesReview.js +141 -0
  144. package/src/simulation/rules/rulesetPresets.js +47 -0
  145. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  147. package/src/simulation/rulesConfig.js +34 -0
  148. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryIndex.js +945 -0
  171. package/src/simulation/track/trackSectors.js +19 -0
  172. package/src/simulation/track/trackStatePolicy.js +24 -0
  173. package/src/simulation/track/trackValidation.js +98 -0
  174. package/src/simulation/trackModel.js +1 -628
  175. package/src/simulation/units.js +6 -4
  176. package/src/simulation/vehicle/contactResolution.js +116 -0
  177. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  178. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  179. package/src/simulation/vehicle/runoffResponse.js +88 -0
  180. package/src/simulation/vehicle/surfacePriority.js +28 -0
  181. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  182. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  183. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  184. package/src/simulation/vehicle/vehicleState.js +186 -0
  185. package/src/simulation/vehicle/wheelSurface.js +150 -0
  186. package/src/simulation/vehicleGeometry.js +1 -0
  187. package/src/simulation/vehiclePhysics.js +1 -104
  188. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  189. package/src/simulation/wheelSurface.js +1 -0
  190. package/src/styles.css +342 -42
  191. package/src/ui/bannerTemplates.js +69 -0
  192. package/src/ui/cameraControlsTemplate.js +20 -0
  193. package/src/ui/carOverviewTemplate.js +33 -0
  194. package/src/ui/componentTemplates.js +20 -426
  195. package/src/ui/raceCanvasTemplate.js +69 -0
  196. package/src/ui/raceControlStatusBanner.js +39 -0
  197. package/src/ui/raceControlsTemplate.js +36 -0
  198. package/src/ui/shellTemplate.js +6 -5
  199. package/src/ui/telemetryDrawerTemplate.js +55 -0
  200. package/src/ui/telemetryTemplates.js +136 -0
  201. package/src/ui/templateUtils.js +23 -0
  202. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,283 @@
1
+ import {
2
+ GENERATED_FALLBACK_ATTEMPTS,
3
+ GENERATED_TRACK_ATTEMPTS,
4
+ GENERATED_TRACK_MAX_LENGTH,
5
+ GENERATED_TRACK_MIN_LENGTH,
6
+ MAX_LOCAL_TURN_RADIANS,
7
+ MAX_SAMPLE_HEADING_DELTA_RADIANS,
8
+ MIN_NON_ADJACENT_ARC_DISTANCE,
9
+ MIN_TRACK_CLEARANCE_MULTIPLIER,
10
+ MIN_TRACK_SHAPE_VARIATION,
11
+ START_STRAIGHT_BLEND_LENGTH,
12
+ START_STRAIGHT_EXIT_LENGTH,
13
+ START_STRAIGHT_GRID_LENGTH,
14
+ START_STRAIGHT_LOCK_EXTRA,
15
+ } from './trackConstants.js';
16
+ import { metersToSimUnits, simUnitsToMeters } from '../units.js';
17
+
18
+ export const PROCEDURAL_TRACK_PROFILES = {
19
+ race: {},
20
+ 'training-short': {
21
+ length: { minMeters: 900, maxMeters: 1800 },
22
+ startStraight: { gridMeters: 0, exitMeters: 80, blendMeters: 80, lockExtraMeters: 0 },
23
+ pitLane: { enabled: false },
24
+ shape: { scale: 0.2, cornerDensity: 1.3, variation: 0.22 },
25
+ validation: {
26
+ minClearanceMultiplier: 1,
27
+ minNonAdjacentArcMeters: 160,
28
+ maxLocalTurnRadians: 1.85,
29
+ },
30
+ attempts: { primary: 80, fallback: 200 },
31
+ },
32
+ 'training-medium': {
33
+ length: { minMeters: 1600, maxMeters: 3000 },
34
+ startStraight: { gridMeters: 80, exitMeters: 140, blendMeters: 120, lockExtraMeters: 0 },
35
+ pitLane: { enabled: false },
36
+ shape: { scale: 0.38, cornerDensity: 1.18, variation: 0.24 },
37
+ validation: {
38
+ minClearanceMultiplier: 1.15,
39
+ minNonAdjacentArcMeters: 260,
40
+ maxLocalTurnRadians: 1.7,
41
+ },
42
+ attempts: { primary: 80, fallback: 200 },
43
+ },
44
+ 'training-technical': {
45
+ length: { minMeters: 800, maxMeters: 1700 },
46
+ startStraight: { gridMeters: 0, exitMeters: 60, blendMeters: 70, lockExtraMeters: 0 },
47
+ pitLane: { enabled: false },
48
+ shape: { scale: 0.18, cornerDensity: 1.55, variation: 0.2 },
49
+ validation: {
50
+ minClearanceMultiplier: 0.9,
51
+ minNonAdjacentArcMeters: 140,
52
+ maxLocalTurnRadians: 2.05,
53
+ },
54
+ attempts: { primary: 100, fallback: 240 },
55
+ },
56
+ };
57
+
58
+ const RACE_DEFAULTS = {
59
+ profile: 'race',
60
+ length: {
61
+ min: GENERATED_TRACK_MIN_LENGTH,
62
+ max: GENERATED_TRACK_MAX_LENGTH,
63
+ },
64
+ startStraight: {
65
+ grid: START_STRAIGHT_GRID_LENGTH,
66
+ exit: START_STRAIGHT_EXIT_LENGTH,
67
+ blend: START_STRAIGHT_BLEND_LENGTH,
68
+ lockExtra: START_STRAIGHT_LOCK_EXTRA,
69
+ },
70
+ pitLane: {
71
+ enabled: true,
72
+ },
73
+ shape: {
74
+ scale: 1,
75
+ cornerDensity: 1,
76
+ variation: MIN_TRACK_SHAPE_VARIATION,
77
+ },
78
+ validation: {
79
+ minClearanceMultiplier: MIN_TRACK_CLEARANCE_MULTIPLIER,
80
+ minShapeVariation: MIN_TRACK_SHAPE_VARIATION,
81
+ minNonAdjacentArcDistance: MIN_NON_ADJACENT_ARC_DISTANCE,
82
+ maxLocalTurnRadians: MAX_LOCAL_TURN_RADIANS,
83
+ maxSampleHeadingDeltaRadians: MAX_SAMPLE_HEADING_DELTA_RADIANS,
84
+ },
85
+ attempts: {
86
+ primary: GENERATED_FALLBACK_ATTEMPTS,
87
+ fallback: GENERATED_TRACK_ATTEMPTS,
88
+ },
89
+ };
90
+
91
+ export function resolveProceduralTrackOptions(options = {}) {
92
+ const requestedProfile = options.profile ?? 'race';
93
+ if (!Object.hasOwn(PROCEDURAL_TRACK_PROFILES, requestedProfile)) {
94
+ throw new Error(`Unsupported procedural track profile: ${requestedProfile}`);
95
+ }
96
+
97
+ const expanded = expandLegacyTrackOptions(options);
98
+ const profile = expandLegacyTrackOptions(PROCEDURAL_TRACK_PROFILES[requestedProfile]);
99
+ const merged = deepMerge(deepMerge({ profile: requestedProfile }, profile), expanded);
100
+
101
+ const resolved = {
102
+ profile: requestedProfile,
103
+ length: {
104
+ min: positiveMetersToSimUnits(merged.length?.minMeters, RACE_DEFAULTS.length.min, 'length.minMeters'),
105
+ max: positiveMetersToSimUnits(merged.length?.maxMeters, RACE_DEFAULTS.length.max, 'length.maxMeters'),
106
+ },
107
+ startStraight: {
108
+ grid: nonNegativeMetersToSimUnits(merged.startStraight?.gridMeters, RACE_DEFAULTS.startStraight.grid, 'startStraight.gridMeters'),
109
+ exit: nonNegativeMetersToSimUnits(merged.startStraight?.exitMeters, RACE_DEFAULTS.startStraight.exit, 'startStraight.exitMeters'),
110
+ blend: nonNegativeMetersToSimUnits(merged.startStraight?.blendMeters, RACE_DEFAULTS.startStraight.blend, 'startStraight.blendMeters'),
111
+ lockExtra: nonNegativeMetersToSimUnits(merged.startStraight?.lockExtraMeters, RACE_DEFAULTS.startStraight.lockExtra, 'startStraight.lockExtraMeters'),
112
+ },
113
+ pitLane: {
114
+ enabled: merged.pitLane?.enabled !== false,
115
+ },
116
+ shape: {
117
+ scale: positiveNumber(merged.shape?.scale, RACE_DEFAULTS.shape.scale, 'shape.scale'),
118
+ cornerDensity: positiveNumber(merged.shape?.cornerDensity, RACE_DEFAULTS.shape.cornerDensity, 'shape.cornerDensity'),
119
+ variation: nonNegativeNumber(merged.shape?.variation, RACE_DEFAULTS.shape.variation, 'shape.variation'),
120
+ },
121
+ validation: {
122
+ minClearanceMultiplier: nonNegativeNumber(
123
+ merged.validation?.minClearanceMultiplier,
124
+ RACE_DEFAULTS.validation.minClearanceMultiplier,
125
+ 'validation.minClearanceMultiplier',
126
+ ),
127
+ minShapeVariation: nonNegativeNumber(
128
+ merged.validation?.minShapeVariation ?? merged.shape?.variation,
129
+ RACE_DEFAULTS.validation.minShapeVariation,
130
+ 'validation.minShapeVariation',
131
+ ),
132
+ minNonAdjacentArcDistance: nonNegativeMetersToSimUnits(
133
+ merged.validation?.minNonAdjacentArcMeters,
134
+ RACE_DEFAULTS.validation.minNonAdjacentArcDistance,
135
+ 'validation.minNonAdjacentArcMeters',
136
+ ),
137
+ maxLocalTurnRadians: positiveNumber(
138
+ merged.validation?.maxLocalTurnRadians,
139
+ RACE_DEFAULTS.validation.maxLocalTurnRadians,
140
+ 'validation.maxLocalTurnRadians',
141
+ ),
142
+ maxSampleHeadingDeltaRadians: positiveNumber(
143
+ merged.validation?.maxSampleHeadingDeltaRadians,
144
+ RACE_DEFAULTS.validation.maxSampleHeadingDeltaRadians,
145
+ 'validation.maxSampleHeadingDeltaRadians',
146
+ ),
147
+ },
148
+ attempts: {
149
+ primary: positiveInteger(merged.attempts?.primary, RACE_DEFAULTS.attempts.primary, 'attempts.primary'),
150
+ fallback: positiveInteger(merged.attempts?.fallback, RACE_DEFAULTS.attempts.fallback, 'attempts.fallback'),
151
+ },
152
+ };
153
+
154
+ if (resolved.length.min > resolved.length.max) {
155
+ throw new Error('Procedural track length.minMeters must be less than or equal to length.maxMeters.');
156
+ }
157
+ resolved.cacheKey = stableTrackGenerationCacheKey(resolved);
158
+ return resolved;
159
+ }
160
+
161
+ export function stableTrackGenerationCacheKey(options) {
162
+ return JSON.stringify({
163
+ profile: options.profile,
164
+ length: {
165
+ minMeters: roundMeters(options.length.min),
166
+ maxMeters: roundMeters(options.length.max),
167
+ },
168
+ startStraight: {
169
+ gridMeters: roundMeters(options.startStraight.grid),
170
+ exitMeters: roundMeters(options.startStraight.exit),
171
+ blendMeters: roundMeters(options.startStraight.blend),
172
+ lockExtraMeters: roundMeters(options.startStraight.lockExtra),
173
+ },
174
+ pitLane: { enabled: options.pitLane.enabled },
175
+ shape: {
176
+ scale: roundNumber(options.shape.scale),
177
+ cornerDensity: roundNumber(options.shape.cornerDensity),
178
+ variation: roundNumber(options.shape.variation),
179
+ },
180
+ validation: {
181
+ minClearanceMultiplier: roundNumber(options.validation.minClearanceMultiplier),
182
+ minShapeVariation: roundNumber(options.validation.minShapeVariation),
183
+ minNonAdjacentArcMeters: roundMeters(options.validation.minNonAdjacentArcDistance),
184
+ maxLocalTurnRadians: roundNumber(options.validation.maxLocalTurnRadians),
185
+ maxSampleHeadingDeltaRadians: roundNumber(options.validation.maxSampleHeadingDeltaRadians),
186
+ },
187
+ attempts: options.attempts,
188
+ });
189
+ }
190
+
191
+ function expandLegacyTrackOptions(options = {}) {
192
+ const next = { ...options };
193
+ if (options.minLengthMeters != null || options.maxLengthMeters != null) {
194
+ next.length = {
195
+ ...(options.length ?? {}),
196
+ ...(options.minLengthMeters != null ? { minMeters: options.minLengthMeters } : {}),
197
+ ...(options.maxLengthMeters != null ? { maxMeters: options.maxLengthMeters } : {}),
198
+ };
199
+ }
200
+ if (options.startStraightMeters != null) {
201
+ next.startStraight = {
202
+ ...(options.startStraight ?? {}),
203
+ gridMeters: options.startStraightMeters,
204
+ exitMeters: options.startStraightMeters,
205
+ };
206
+ }
207
+ if (options.includePitLane != null) {
208
+ next.pitLane = {
209
+ ...(options.pitLane ?? {}),
210
+ enabled: Boolean(options.includePitLane),
211
+ };
212
+ }
213
+ return next;
214
+ }
215
+
216
+ function deepMerge(base, override) {
217
+ const result = { ...base };
218
+ Object.entries(override ?? {}).forEach(([key, value]) => {
219
+ if (isPlainObject(value) && isPlainObject(result[key])) {
220
+ result[key] = deepMerge(result[key], value);
221
+ return;
222
+ }
223
+ if (value !== undefined) result[key] = value;
224
+ });
225
+ return result;
226
+ }
227
+
228
+ function isPlainObject(value) {
229
+ return value != null && typeof value === 'object' && !Array.isArray(value);
230
+ }
231
+
232
+ function positiveMetersToSimUnits(value, fallback, label) {
233
+ if (value == null) return fallback;
234
+ const number = Number(value);
235
+ if (!Number.isFinite(number) || number <= 0) {
236
+ throw new Error(`Procedural track ${label} must be a positive number.`);
237
+ }
238
+ return metersToSimUnits(number);
239
+ }
240
+
241
+ function nonNegativeMetersToSimUnits(value, fallback, label) {
242
+ if (value == null) return fallback;
243
+ const number = Number(value);
244
+ if (!Number.isFinite(number) || number < 0) {
245
+ throw new Error(`Procedural track ${label} must be a non-negative number.`);
246
+ }
247
+ return metersToSimUnits(number);
248
+ }
249
+
250
+ function positiveNumber(value, fallback, label) {
251
+ if (value == null) return fallback;
252
+ const number = Number(value);
253
+ if (!Number.isFinite(number) || number <= 0) {
254
+ throw new Error(`Procedural track ${label} must be a positive number.`);
255
+ }
256
+ return number;
257
+ }
258
+
259
+ function nonNegativeNumber(value, fallback, label) {
260
+ if (value == null) return fallback;
261
+ const number = Number(value);
262
+ if (!Number.isFinite(number) || number < 0) {
263
+ throw new Error(`Procedural track ${label} must be a non-negative number.`);
264
+ }
265
+ return number;
266
+ }
267
+
268
+ function positiveInteger(value, fallback, label) {
269
+ if (value == null) return fallback;
270
+ const number = Number(value);
271
+ if (!Number.isInteger(number) || number <= 0) {
272
+ throw new Error(`Procedural track ${label} must be a positive integer.`);
273
+ }
274
+ return number;
275
+ }
276
+
277
+ function roundMeters(value) {
278
+ return roundNumber(simUnitsToMeters(value));
279
+ }
280
+
281
+ function roundNumber(value) {
282
+ return Math.round(value * 1e6) / 1e6;
283
+ }
@@ -0,0 +1,241 @@
1
+ import { clamp } from '../simMath.js';
2
+ import { WORLD, PIT_WORLD_PADDING } from './trackConstants.js';
3
+
4
+ export function distance(a, b) {
5
+ return Math.hypot(a.x - b.x, a.y - b.y);
6
+ }
7
+
8
+ export function orientation(a, b, c) {
9
+ return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
10
+ }
11
+
12
+ export function segmentBoxesOverlap(a, b, c, d) {
13
+ return (
14
+ Math.max(Math.min(a.x, b.x), Math.min(c.x, d.x)) <= Math.min(Math.max(a.x, b.x), Math.max(c.x, d.x)) &&
15
+ Math.max(Math.min(a.y, b.y), Math.min(c.y, d.y)) <= Math.min(Math.max(a.y, b.y), Math.max(c.y, d.y))
16
+ );
17
+ }
18
+
19
+ export function segmentsIntersect(a, b, c, d) {
20
+ if (!segmentBoxesOverlap(a, b, c, d)) return false;
21
+ const abC = orientation(a, b, c);
22
+ const abD = orientation(a, b, d);
23
+ const cdA = orientation(c, d, a);
24
+ const cdB = orientation(c, d, b);
25
+ return abC * abD < 0 && cdA * cdB < 0;
26
+ }
27
+
28
+ export function distanceForwardAlongTrack(from, to, totalLength) {
29
+ return to >= from ? to - from : totalLength - from + to;
30
+ }
31
+
32
+ export function smoothstep(value) {
33
+ const amount = clamp(value, 0, 1);
34
+ return amount * amount * (3 - 2 * amount);
35
+ }
36
+
37
+ export function blendPoint(original, target, amount) {
38
+ return {
39
+ ...original,
40
+ x: original.x + (target.x - original.x) * amount,
41
+ y: original.y + (target.y - original.y) * amount,
42
+ };
43
+ }
44
+
45
+ export function clonePoint(point) {
46
+ return {
47
+ x: point.x,
48
+ y: point.y,
49
+ heading: point.heading,
50
+ distance: point.distance,
51
+ };
52
+ }
53
+
54
+ export function normalizeVector(vector) {
55
+ const length = Math.hypot(vector.x, vector.y) || 1;
56
+ return {
57
+ x: vector.x / length,
58
+ y: vector.y / length,
59
+ length,
60
+ };
61
+ }
62
+
63
+ export function headingVector(heading) {
64
+ return {
65
+ x: Math.cos(heading),
66
+ y: Math.sin(heading),
67
+ };
68
+ }
69
+
70
+ export function dotVectors(first, second) {
71
+ return first.x * second.x + first.y * second.y;
72
+ }
73
+
74
+ export function angleBetweenVectors(first, second) {
75
+ const firstUnit = normalizeVector(first);
76
+ const secondUnit = normalizeVector(second);
77
+ return Math.acos(clamp(dotVectors(firstUnit, secondUnit), -1, 1));
78
+ }
79
+
80
+ export function signedLateralOffsetToPoint(trackPoint, point) {
81
+ return (point.x - trackPoint.x) * trackPoint.normalX + (point.y - trackPoint.y) * trackPoint.normalY;
82
+ }
83
+
84
+ export function interpolatePitPoint(start, forward, distanceAlong) {
85
+ return {
86
+ x: start.x + forward.x * distanceAlong,
87
+ y: start.y + forward.y * distanceAlong,
88
+ };
89
+ }
90
+
91
+ export function projectPitPoint(startPoint, forward, normal, distanceAlong, lateralOffset) {
92
+ return {
93
+ x: startPoint.x + forward.x * distanceAlong + normal.x * lateralOffset,
94
+ y: startPoint.y + forward.y * distanceAlong + normal.y * lateralOffset,
95
+ };
96
+ }
97
+
98
+ export function pointWorldClearance(point) {
99
+ return Math.min(
100
+ point.x - PIT_WORLD_PADDING,
101
+ WORLD.width - PIT_WORLD_PADDING - point.x,
102
+ point.y - PIT_WORLD_PADDING,
103
+ WORLD.height - PIT_WORLD_PADDING - point.y,
104
+ );
105
+ }
106
+
107
+ export function sampleCubicBezier(p0, p1, p2, p3, steps) {
108
+ return Array.from({ length: steps + 1 }, (_, index) => {
109
+ const t = index / steps;
110
+ const mt = 1 - t;
111
+ const mt2 = mt * mt;
112
+ const t2 = t * t;
113
+ return {
114
+ x: mt2 * mt * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t2 * t * p3.x,
115
+ y: mt2 * mt * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t2 * t * p3.y,
116
+ };
117
+ });
118
+ }
119
+
120
+ export function expandBounds(bounds, point) {
121
+ if (!point) return bounds;
122
+ return {
123
+ minX: Math.min(bounds.minX, point.x),
124
+ maxX: Math.max(bounds.maxX, point.x),
125
+ minY: Math.min(bounds.minY, point.y),
126
+ maxY: Math.max(bounds.maxY, point.y),
127
+ };
128
+ }
129
+
130
+ export function createPointBounds(points, padding = 0) {
131
+ const bounds = points.filter(Boolean).reduce(
132
+ (current, point) => expandBounds(current, point),
133
+ { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity },
134
+ );
135
+ if (!Number.isFinite(bounds.minX)) return null;
136
+ return {
137
+ minX: bounds.minX - padding,
138
+ maxX: bounds.maxX + padding,
139
+ minY: bounds.minY - padding,
140
+ maxY: bounds.maxY + padding,
141
+ };
142
+ }
143
+
144
+ export function pointInsideBounds(point, bounds) {
145
+ if (!bounds) return true;
146
+ return (
147
+ point.x >= bounds.minX &&
148
+ point.x <= bounds.maxX &&
149
+ point.y >= bounds.minY &&
150
+ point.y <= bounds.maxY
151
+ );
152
+ }
153
+
154
+ export function projectPointToSegment(position, start, end) {
155
+ const dx = end.x - start.x;
156
+ const dy = end.y - start.y;
157
+ const lengthSquared = dx * dx + dy * dy;
158
+ const amount = lengthSquared > 0
159
+ ? clamp(((position.x - start.x) * dx + (position.y - start.y) * dy) / lengthSquared, 0, 1)
160
+ : 0;
161
+ const projected = {
162
+ x: start.x + dx * amount,
163
+ y: start.y + dy * amount,
164
+ };
165
+ const length = Math.sqrt(lengthSquared);
166
+ const heading = Math.atan2(dy, dx);
167
+ const normalX = length > 0 ? -dy / length : 0;
168
+ const normalY = length > 0 ? dx / length : 1;
169
+ const signedOffset = (position.x - projected.x) * normalX + (position.y - projected.y) * normalY;
170
+
171
+ return {
172
+ point: projected,
173
+ amount,
174
+ length,
175
+ heading,
176
+ normalX,
177
+ normalY,
178
+ signedOffset,
179
+ crossTrackError: Math.abs(signedOffset),
180
+ };
181
+ }
182
+
183
+ export function nearestPointOnPolyline(points, position, segmentIndices = null) {
184
+ if (!Array.isArray(points) || points.length < 2) return null;
185
+ const restrictedSegments = Array.isArray(segmentIndices) && segmentIndices.length > 0
186
+ ? [...new Set(segmentIndices)].filter((index) => Number.isInteger(index) && index >= 0 && index < points.length - 1)
187
+ : null;
188
+ const cumulativeDistances = restrictedSegments ? createPolylineCumulativeDistances(points) : null;
189
+ let best = null;
190
+ let distanceBefore = 0;
191
+ let totalLength = 0;
192
+
193
+ const segmentCount = restrictedSegments?.length ?? (points.length - 1);
194
+ for (let segmentOffset = 0; segmentOffset < segmentCount; segmentOffset += 1) {
195
+ const index = restrictedSegments ? restrictedSegments[segmentOffset] : segmentOffset;
196
+ const current = points[index];
197
+ const next = points[index + 1];
198
+ const projection = projectPointToSegment(position, current, next);
199
+ if (!best || projection.crossTrackError < best.crossTrackError) {
200
+ best = {
201
+ ...projection,
202
+ segmentIndex: index,
203
+ distanceAlong: (restrictedSegments ? cumulativeDistances[index] : distanceBefore) + projection.length * projection.amount,
204
+ };
205
+ }
206
+ if (!restrictedSegments) distanceBefore += projection.length;
207
+ totalLength += projection.length;
208
+ }
209
+
210
+ if (!best) return null;
211
+ return {
212
+ ...best,
213
+ totalLength: restrictedSegments
214
+ ? cumulativeDistances[cumulativeDistances.length - 1]
215
+ : totalLength,
216
+ };
217
+ }
218
+
219
+ function createPolylineCumulativeDistances(points) {
220
+ const distances = new Array(points.length).fill(0);
221
+ for (let index = 1; index < points.length; index += 1) {
222
+ distances[index] = distances[index - 1] + distance(points[index - 1], points[index]);
223
+ }
224
+ return distances;
225
+ }
226
+
227
+ export function pointIsInsidePolygon(point, polygon) {
228
+ if (!Array.isArray(polygon) || polygon.length < 3) return false;
229
+ let inside = false;
230
+
231
+ for (let currentIndex = 0, previousIndex = polygon.length - 1; currentIndex < polygon.length; previousIndex = currentIndex, currentIndex += 1) {
232
+ const current = polygon[currentIndex];
233
+ const previous = polygon[previousIndex];
234
+ const crossesY = (current.y > point.y) !== (previous.y > point.y);
235
+ if (!crossesY) continue;
236
+ const xAtY = ((previous.x - current.x) * (point.y - current.y)) / (previous.y - current.y) + current.x;
237
+ if (point.x < xAtY) inside = !inside;
238
+ }
239
+
240
+ return inside;
241
+ }