@inventure71/paddockjs 1.0.0 → 2.0.1

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 (203) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +219 -12
  4. package/docs/architecture.md +126 -45
  5. package/docs/concepts.md +9 -5
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +317 -22
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +10 -0
  10. package/docs/learnings.md +22 -0
  11. package/docs/rules.md +71 -14
  12. package/docs/sense_contract.md +59 -0
  13. package/docs/system_specs.md +75 -13
  14. package/docs/training.md +280 -32
  15. package/examples/python/README.md +62 -0
  16. package/examples/python/base_policy_server.py +191 -0
  17. package/examples/python/requirements.txt +2 -0
  18. package/package.json +12 -4
  19. package/src/api/PaddockSimulatorController.js +6 -33
  20. package/src/app/BrowserExpertAdapter.js +76 -9
  21. package/src/app/BrowserExpertExternalRenderer.js +303 -0
  22. package/src/app/F1SimulatorApp.js +291 -1630
  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 +4 -0
  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 +20 -0
  48. package/src/config/restartOptions.js +34 -0
  49. package/src/environment/actions.js +13 -5
  50. package/src/environment/controllerLoop.js +261 -0
  51. package/src/environment/episode.js +98 -2
  52. package/src/environment/evaluation.js +54 -32
  53. package/src/environment/index.d.ts +376 -10
  54. package/src/environment/index.js +1 -0
  55. package/src/environment/metrics.js +159 -0
  56. package/src/environment/observationOptions.js +22 -1
  57. package/src/environment/observationVector.js +200 -0
  58. package/src/environment/observations.js +199 -141
  59. package/src/environment/options.js +37 -5
  60. package/src/environment/recorder.js +13 -6
  61. package/src/environment/rewards.js +13 -2
  62. package/src/environment/runtime.js +272 -23
  63. package/src/environment/scenarios.js +57 -9
  64. package/src/environment/sensors/bodySenses.js +23 -0
  65. package/src/environment/sensors/boundarySenses.js +17 -0
  66. package/src/environment/sensors/carRays.js +75 -0
  67. package/src/environment/sensors/contactSenses.js +32 -0
  68. package/src/environment/sensors/index.js +157 -0
  69. package/src/environment/sensors/indexedRayBands.js +103 -0
  70. package/src/environment/sensors/nearbyCars.js +52 -0
  71. package/src/environment/sensors/opponentRadar.js +75 -0
  72. package/src/environment/sensors/rayConfig.js +110 -0
  73. package/src/environment/sensors/rayDefaults.js +6 -0
  74. package/src/environment/sensors/rayGeometry.js +60 -0
  75. package/src/environment/sensors/rayGuards.js +19 -0
  76. package/src/environment/sensors/sensorTargets.js +66 -0
  77. package/src/environment/sensors/surfaceRays.js +360 -0
  78. package/src/environment/sensors/trackRays.js +424 -0
  79. package/src/environment/sensors.js +1 -367
  80. package/src/environment/specs.js +159 -26
  81. package/src/environment/workerProtocol.js +13 -1
  82. package/src/index.d.ts +171 -6
  83. package/src/index.js +6 -32
  84. package/src/rendering/proceduralTrackAsset.js +20 -823
  85. package/src/rendering/renderSnapshot.js +38 -12
  86. package/src/rendering/track/finishLineRenderer.js +34 -0
  87. package/src/rendering/track/gridRenderer.js +47 -0
  88. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  89. package/src/rendering/track/pitLaneRenderer.js +248 -0
  90. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  91. package/src/rendering/track/trackRenderConstants.js +38 -0
  92. package/src/rendering/track/trackRenderGeometry.js +94 -0
  93. package/src/simulation/collisionGeometry.js +1 -1
  94. package/src/simulation/driver/driverControlConstants.js +28 -0
  95. package/src/simulation/driver/driverController.js +47 -0
  96. package/src/simulation/driver/driverInput.js +28 -0
  97. package/src/simulation/driver/driverMath.js +6 -0
  98. package/src/simulation/driver/driverPersonality.js +25 -0
  99. package/src/simulation/driver/edgeRecovery.js +48 -0
  100. package/src/simulation/driver/racingControls.js +399 -0
  101. package/src/simulation/driver/racingLinePlan.js +166 -0
  102. package/src/simulation/driver/recoveryDynamics.js +38 -0
  103. package/src/simulation/driver/rejoinControls.js +213 -0
  104. package/src/simulation/driver/safetyCarControls.js +35 -0
  105. package/src/simulation/driver/trafficScan.js +51 -0
  106. package/src/simulation/driverController.js +4 -619
  107. package/src/simulation/participants/participantInteractions.js +120 -0
  108. package/src/simulation/pit/pitFlow.js +239 -0
  109. package/src/simulation/pit/pitIntent.js +92 -0
  110. package/src/simulation/pit/pitKinematics.js +24 -0
  111. package/src/simulation/pit/pitOccupancy.js +47 -0
  112. package/src/simulation/pit/pitPenaltyService.js +71 -0
  113. package/src/simulation/pit/pitQueue.js +60 -0
  114. package/src/simulation/pit/pitRouteMovement.js +167 -0
  115. package/src/simulation/pit/pitRouting.js +319 -0
  116. package/src/simulation/pit/pitService.js +43 -0
  117. package/src/simulation/pit/pitServiceConstants.js +21 -0
  118. package/src/simulation/pit/pitServiceProfile.js +45 -0
  119. package/src/simulation/pit/pitSnapshots.js +61 -0
  120. package/src/simulation/pit/pitState.js +113 -0
  121. package/src/simulation/pit/pitTireService.js +94 -0
  122. package/src/simulation/race/classification.js +104 -0
  123. package/src/simulation/race/gridPenalties.js +37 -0
  124. package/src/simulation/race/raceControlState.js +38 -0
  125. package/src/simulation/race/raceDistance.js +32 -0
  126. package/src/simulation/race/raceFinish.js +65 -0
  127. package/src/simulation/race/raceLifecycle.js +67 -0
  128. package/src/simulation/race/raceOrder.js +113 -0
  129. package/src/simulation/race/raceProgress.js +129 -0
  130. package/src/simulation/race/raceSetup.js +93 -0
  131. package/src/simulation/race/raceStep.js +80 -0
  132. package/src/simulation/race/redFlag.js +15 -0
  133. package/src/simulation/race/retirements.js +44 -0
  134. package/src/simulation/race/safetyCar.js +92 -0
  135. package/src/simulation/race/stalledDnf.js +77 -0
  136. package/src/simulation/raceSimulation.js +231 -2817
  137. package/src/simulation/replay/replayGhosts.js +128 -0
  138. package/src/simulation/rules/moduleConfig.js +72 -0
  139. package/src/simulation/rules/penaltyConfig.js +99 -0
  140. package/src/simulation/rules/penaltyEvents.js +18 -0
  141. package/src/simulation/rules/penaltyStats.js +26 -0
  142. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  143. package/src/simulation/rules/ruleDefaults.js +85 -0
  144. package/src/simulation/rules/rulesReview.js +144 -0
  145. package/src/simulation/rules/rulesetPresets.js +47 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  147. package/src/simulation/rulesConfig.js +5 -323
  148. package/src/simulation/snapshots/raceSnapshots.js +193 -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/trackQueryGrid.js +94 -0
  171. package/src/simulation/track/trackQueryIndex.js +659 -0
  172. package/src/simulation/track/trackQueryPitIndex.js +140 -0
  173. package/src/simulation/track/trackQueryScratch.js +46 -0
  174. package/src/simulation/track/trackQueryStats.js +28 -0
  175. package/src/simulation/track/trackSectors.js +19 -0
  176. package/src/simulation/track/trackStatePolicy.js +24 -0
  177. package/src/simulation/track/trackValidation.js +98 -0
  178. package/src/simulation/trackModel.js +1 -1706
  179. package/src/simulation/vehicle/contactResolution.js +181 -0
  180. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  181. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  182. package/src/simulation/vehicle/runoffResponse.js +87 -0
  183. package/src/simulation/vehicle/surfacePriority.js +28 -0
  184. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  185. package/src/simulation/vehicle/vehicleKinematics.js +37 -0
  186. package/src/simulation/vehicle/vehiclePhysics.js +364 -0
  187. package/src/simulation/vehicle/vehicleSnapshots.js +327 -0
  188. package/src/simulation/vehicle/vehicleState.js +220 -0
  189. package/src/simulation/vehicle/wheelSurface.js +152 -0
  190. package/src/simulation/vehicleGeometry.js +1 -247
  191. package/src/simulation/vehiclePhysics.js +1 -136
  192. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  193. package/src/simulation/wheelSurface.js +1 -338
  194. package/src/styles.css +42 -0
  195. package/src/ui/cameraControlsTemplate.js +20 -0
  196. package/src/ui/carOverviewTemplate.js +33 -0
  197. package/src/ui/componentTemplates.js +14 -360
  198. package/src/ui/raceCanvasTemplate.js +69 -0
  199. package/src/ui/raceControlsTemplate.js +36 -0
  200. package/src/ui/shellTemplate.js +5 -4
  201. package/src/ui/telemetryDrawerTemplate.js +55 -0
  202. package/src/ui/telemetryTemplates.js +136 -0
  203. package/src/ui/timingTowerTemplate.js +34 -0
@@ -0,0 +1,140 @@
1
+ import { createSpatialGrid, insertIdIntoGridBounds, candidateIdsFromGrid } from './trackQueryGrid.js';
2
+ import { recordFallback, recordStat } from './trackQueryStats.js';
3
+
4
+ export function createPitQueryIndex(track, cellSize) {
5
+ const pitLane = track.pitLane;
6
+ if (!pitLane?.enabled) return null;
7
+ const bounds = {
8
+ minX: pitLane.bounds.minX - cellSize,
9
+ maxX: pitLane.bounds.maxX + cellSize,
10
+ minY: pitLane.bounds.minY - cellSize,
11
+ maxY: pitLane.bounds.maxY + cellSize,
12
+ };
13
+ const roadGrid = createSpatialGrid(bounds, cellSize);
14
+ const boxGrid = createSpatialGrid(bounds, cellSize);
15
+ const roadSegments = [];
16
+ const routes = {};
17
+
18
+ [
19
+ ['entry', pitLane.entry?.roadCenterline],
20
+ ['main', pitLane.mainLane?.points],
21
+ ['working', pitLane.workingLane?.points],
22
+ ['exit', pitLane.exit?.roadCenterline],
23
+ ].forEach(([routeId, points]) => {
24
+ routes[routeId] = { start: roadSegments.length };
25
+ indexPitRoute(roadGrid, roadSegments, routeId, points, cellSize);
26
+ routes[routeId].end = roadSegments.length;
27
+ });
28
+
29
+ const boxCandidates = [];
30
+ [
31
+ ...(pitLane.serviceAreas ?? []).flatMap((area) => [
32
+ { type: 'service-area', target: area, polygon: area.corners },
33
+ { type: 'service-queue', target: area, polygon: area.queueCorners },
34
+ ]),
35
+ ...(pitLane.boxes ?? []).map((box) => ({ type: 'garage-box', target: box, polygon: box.corners })),
36
+ ]
37
+ .filter((candidate) => Array.isArray(candidate.polygon) && candidate.polygon.length >= 3)
38
+ .forEach((candidate) => {
39
+ const id = boxCandidates.length;
40
+ boxCandidates.push(candidate);
41
+ insertIdIntoGridBounds(boxGrid, id, polygonBounds(candidate.polygon, cellSize));
42
+ });
43
+
44
+ return {
45
+ roadGrid,
46
+ boxGrid,
47
+ routes,
48
+ roadSegments,
49
+ boxCandidates,
50
+ };
51
+ }
52
+
53
+ export function queryPitBoxCandidates(track, position) {
54
+ const index = track?.queryIndex;
55
+ const pit = index?.pit;
56
+ if (!pit?.boxGrid) {
57
+ recordFallback(index, 'pitFallbackReasons', 'missing-box-index');
58
+ return null;
59
+ }
60
+ if (!finitePoint(position)) {
61
+ recordFallback(index, 'pitFallbackReasons', 'invalid-position');
62
+ return null;
63
+ }
64
+ index.stats.pitQueries += 1;
65
+ const ids = candidateIdsFromGrid(index, pit.boxGrid, position, 1);
66
+ if (!ids.length) {
67
+ recordStat(index, 'pitPaths', 'box-grid-miss');
68
+ return [];
69
+ }
70
+ recordStat(index, 'pitPaths', 'box-grid-hit');
71
+ return ids.map((id) => pit.boxCandidates[id]).filter(Boolean);
72
+ }
73
+
74
+ export function queryPitRoadSegmentCandidates(track, routeId, position) {
75
+ const candidatesByRoute = queryPitRoadSegmentCandidatesByRoute(track, position);
76
+ return candidatesByRoute?.[routeId] ?? candidatesByRoute;
77
+ }
78
+
79
+ export function queryPitRoadSegmentCandidatesByRoute(track, position) {
80
+ const index = track?.queryIndex;
81
+ const pit = index?.pit;
82
+ if (!pit?.roadGrid) {
83
+ recordFallback(index, 'pitFallbackReasons', 'missing-road-index');
84
+ return null;
85
+ }
86
+ if (!finitePoint(position)) {
87
+ recordFallback(index, 'pitFallbackReasons', 'invalid-position');
88
+ return null;
89
+ }
90
+ index.stats.pitQueries += 1;
91
+ const ids = candidateIdsFromGrid(index, pit.roadGrid, position, 1);
92
+ if (!ids.length) {
93
+ recordStat(index, 'pitPaths', 'road-grid-miss');
94
+ return null;
95
+ }
96
+ const candidatesByRoute = { entry: [], main: [], working: [], exit: [] };
97
+ ids.forEach((id) => {
98
+ const segment = pit.roadSegments[id];
99
+ if (!segment || !candidatesByRoute[segment.routeId]) return;
100
+ candidatesByRoute[segment.routeId].push(segment.segmentIndex);
101
+ });
102
+ recordStat(index, 'pitPaths', 'road-grid-hit');
103
+ return candidatesByRoute;
104
+ }
105
+
106
+ function indexPitRoute(grid, roadSegments, routeId, points, expansion) {
107
+ if (!Array.isArray(points) || points.length < 2) return;
108
+ for (let index = 0; index < points.length - 1; index += 1) {
109
+ const start = points[index];
110
+ const end = points[index + 1];
111
+ const id = roadSegments.length;
112
+ roadSegments.push({ routeId, segmentIndex: index });
113
+ const bounds = {
114
+ minX: Math.min(start.x, end.x) - expansion,
115
+ maxX: Math.max(start.x, end.x) + expansion,
116
+ minY: Math.min(start.y, end.y) - expansion,
117
+ maxY: Math.max(start.y, end.y) + expansion,
118
+ };
119
+ insertIdIntoGridBounds(grid, id, bounds);
120
+ }
121
+ }
122
+
123
+ function polygonBounds(polygon, expansion) {
124
+ const bounds = polygon.reduce((current, point) => ({
125
+ minX: Math.min(current.minX, point.x),
126
+ maxX: Math.max(current.maxX, point.x),
127
+ minY: Math.min(current.minY, point.y),
128
+ maxY: Math.max(current.maxY, point.y),
129
+ }), { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity });
130
+ return {
131
+ minX: bounds.minX - expansion,
132
+ maxX: bounds.maxX + expansion,
133
+ minY: bounds.minY - expansion,
134
+ maxY: bounds.maxY + expansion,
135
+ };
136
+ }
137
+
138
+ function finitePoint(point) {
139
+ return Number.isFinite(point?.x) && Number.isFinite(point?.y);
140
+ }
@@ -0,0 +1,46 @@
1
+ export function createQueryScratch(index) {
2
+ const segmentCount = Math.max(0, index?.centerline?.segmentCount ?? 0);
3
+ const maxId = Math.max(
4
+ segmentCount,
5
+ index?.pit?.roadSegments?.length ?? 0,
6
+ index?.pit?.boxCandidates?.length ?? 0,
7
+ );
8
+ const grid = index?.segmentGrid ?? index?.grid;
9
+ const cellCount = Math.max(0, (grid?.columns ?? 0) * (grid?.rows ?? 0));
10
+ return {
11
+ candidateMarks: new Uint32Array(Math.max(1, maxId)),
12
+ candidateEpoch: 1,
13
+ sampleMarks: new Uint32Array(Math.max(1, segmentCount)),
14
+ sampleEpoch: 1,
15
+ raySegmentMarks: new Uint32Array(Math.max(1, segmentCount)),
16
+ raySegmentEpoch: 1,
17
+ rayCellMarks: new Uint32Array(Math.max(1, cellCount)),
18
+ rayCellEpoch: 1,
19
+ raySegmentIds: [],
20
+ rayVisitedCells: [],
21
+ rayTraceCache: new Map(),
22
+ };
23
+ }
24
+
25
+ export function ensureQueryScratch(index) {
26
+ if (index.queryScratch) return index.queryScratch;
27
+ index.queryScratch = createQueryScratch(index);
28
+ return index.queryScratch;
29
+ }
30
+
31
+ export function ensureScratchArray(scratch, key, minimumLength) {
32
+ if (!scratch[key] || scratch[key].length < minimumLength) {
33
+ scratch[key] = new Uint32Array(Math.max(1, minimumLength));
34
+ }
35
+ return scratch[key];
36
+ }
37
+
38
+ export function nextScratchEpoch(scratch, key, marks) {
39
+ let epoch = (scratch[key] ?? 0) + 1;
40
+ if (epoch >= 0xFFFFFFFF) {
41
+ marks.fill(0);
42
+ epoch = 1;
43
+ }
44
+ scratch[key] = epoch;
45
+ return epoch;
46
+ }
@@ -0,0 +1,28 @@
1
+ export function createQueryStats() {
2
+ return {
3
+ nearestQueries: 0,
4
+ nearestFallbacks: 0,
5
+ nearestPaths: {},
6
+ nearestFallbackReasons: {
7
+ 'spatial-grid-no-candidates': 0,
8
+ },
9
+ pitQueries: 0,
10
+ pitFallbacks: 0,
11
+ pitPaths: {},
12
+ pitFallbackReasons: {},
13
+ };
14
+ }
15
+
16
+ export function recordFallback(index, bucket, reason) {
17
+ if (!index?.stats) return;
18
+ if (bucket === 'nearestFallbackReasons') index.stats.nearestFallbacks += 1;
19
+ if (bucket === 'pitFallbackReasons') index.stats.pitFallbacks += 1;
20
+ recordStat(index, bucket, reason);
21
+ }
22
+
23
+ export function recordStat(index, bucket, key) {
24
+ if (!index?.stats || !bucket || !key) return;
25
+ const target = index.stats[bucket] ?? {};
26
+ target[key] = (target[key] ?? 0) + 1;
27
+ index.stats[bucket] = target;
28
+ }
@@ -0,0 +1,19 @@
1
+ export function createTrackSectors(totalLength) {
2
+ return Array.from({ length: 3 }, (_, index) => {
3
+ const startRatio = index / 3;
4
+ const endRatio = (index + 1) / 3;
5
+ const start = totalLength * startRatio;
6
+ const end = totalLength * endRatio;
7
+
8
+ return {
9
+ index: index + 1,
10
+ id: `s${index + 1}`,
11
+ label: `S${index + 1}`,
12
+ start,
13
+ end,
14
+ startRatio,
15
+ endRatio,
16
+ length: end - start,
17
+ };
18
+ });
19
+ }
@@ -0,0 +1,24 @@
1
+ import { nearestTrackState } from './trackModel.js';
2
+
3
+ export function pitOverrideAllowedForCar(car) {
4
+ if (!car?.environmentControlled) return true;
5
+ const status = car.pitStop?.status ?? null;
6
+ const pitRouteActive = status != null && status !== 'pending' && status !== 'completed';
7
+ const pitIntentCommitted = Number(car.pitStop?.intent ?? car.pitIntent ?? 0) >= 2;
8
+ return pitRouteActive || pitIntentCommitted;
9
+ }
10
+
11
+ export function nearestTrackStateForCar(
12
+ track,
13
+ car,
14
+ position = car,
15
+ progressHint = car?.progress ?? null,
16
+ options = {},
17
+ ) {
18
+ const resolvedAllowPitOverride = options.allowPitOverride ?? pitOverrideAllowedForCar(car);
19
+ return nearestTrackState(track, position, progressHint, {
20
+ indexMode: options.indexMode ?? 'legacy',
21
+ ...options,
22
+ allowPitOverride: resolvedAllowPitOverride,
23
+ });
24
+ }
@@ -0,0 +1,98 @@
1
+ import { normalizeAngle } from '../simMath.js';
2
+ import { GENERATED_TRACK_MAX_LENGTH, GENERATED_TRACK_MIN_LENGTH, MAX_LOCAL_TURN_RADIANS, MAX_SAMPLE_HEADING_DELTA_RADIANS, MIN_NON_ADJACENT_ARC_DISTANCE, MIN_TRACK_CLEARANCE_MULTIPLIER, MIN_TRACK_SHAPE_VARIATION, TRACK_BOUNDARY_PADDING, WORLD } from './trackConstants.js';
3
+ import { distance, segmentsIntersect } from './trackMath.js';
4
+
5
+ export function hasSelfIntersections(samples) {
6
+ const points = samples.slice(0, -1).filter((_, index) => index % 12 === 0);
7
+ for (let first = 0; first < points.length - 1; first += 1) {
8
+ for (let second = first + 2; second < points.length - 1; second += 1) {
9
+ const sharesLoopClosure = first === 0 && second >= points.length - 3;
10
+ if (sharesLoopClosure) continue;
11
+ if (segmentsIntersect(points[first], points[first + 1], points[second], points[second + 1])) return true;
12
+ }
13
+ }
14
+ return false;
15
+ }
16
+
17
+ export function samplesStayInsideWorld(samples) {
18
+ return samples.every((sample) => (
19
+ sample.x >= TRACK_BOUNDARY_PADDING &&
20
+ sample.x <= WORLD.width - TRACK_BOUNDARY_PADDING &&
21
+ sample.y >= TRACK_BOUNDARY_PADDING &&
22
+ sample.y <= WORLD.height - TRACK_BOUNDARY_PADDING
23
+ ));
24
+ }
25
+
26
+ export function hasEnoughTrackClearance(samples, totalLength, minimumClearance, minimumNonAdjacentArcDistance = MIN_NON_ADJACENT_ARC_DISTANCE) {
27
+ const points = samples.slice(0, -1).filter((_, index) => index % 24 === 0);
28
+ const minimumClearanceSquared = minimumClearance * minimumClearance;
29
+ for (let first = 0; first < points.length; first += 1) {
30
+ for (let second = first + 1; second < points.length; second += 1) {
31
+ const arcDistance = Math.abs(points[second].distance - points[first].distance);
32
+ const loopDistance = Math.min(arcDistance, totalLength - arcDistance);
33
+ if (loopDistance < minimumNonAdjacentArcDistance) continue;
34
+ const dx = points[first].x - points[second].x;
35
+ const dy = points[first].y - points[second].y;
36
+ if (dx * dx + dy * dy < minimumClearanceSquared) return false;
37
+ }
38
+ }
39
+ return true;
40
+ }
41
+
42
+ export function hasReasonableTurnSharpness(samples, options = {}) {
43
+ const maxSampleHeadingDeltaRadians = options.maxSampleHeadingDeltaRadians ?? MAX_SAMPLE_HEADING_DELTA_RADIANS;
44
+ const maxLocalTurnRadians = options.maxLocalTurnRadians ?? MAX_LOCAL_TURN_RADIANS;
45
+ const usableSamples = samples.slice(0, -1);
46
+ const windows = [30, 36];
47
+ const step = 6;
48
+
49
+ for (let index = 0; index < usableSamples.length; index += 1) {
50
+ const current = usableSamples[index];
51
+ const next = usableSamples[(index + 1) % usableSamples.length];
52
+ if (Math.abs(normalizeAngle(next.heading - current.heading)) > maxSampleHeadingDeltaRadians) {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ for (let index = 0; index < usableSamples.length; index += step) {
58
+ for (const window of windows) {
59
+ let accumulatedTurn = 0;
60
+ for (let offset = 0; offset < window; offset += step) {
61
+ const current = usableSamples[(index + offset) % usableSamples.length];
62
+ const next = usableSamples[(index + offset + step) % usableSamples.length];
63
+ accumulatedTurn += Math.abs(normalizeAngle(next.heading - current.heading));
64
+ }
65
+ if (accumulatedTurn > maxLocalTurnRadians) return false;
66
+ }
67
+ }
68
+
69
+ return true;
70
+ }
71
+
72
+ export function hasEnoughShapeVariation(controls, minimumVariation = MIN_TRACK_SHAPE_VARIATION) {
73
+ const center = { x: WORLD.width / 2, y: WORLD.height / 2 };
74
+ const radii = controls.map((point) => distance(point, center));
75
+ const mean = radii.reduce((total, radius) => total + radius, 0) / Math.max(1, radii.length);
76
+ if (mean <= 0) return false;
77
+ const variance = radii.reduce((total, radius) => total + (radius - mean) ** 2, 0) / Math.max(1, radii.length);
78
+ return Math.sqrt(variance) / mean > minimumVariation;
79
+ }
80
+
81
+ export function isValidProceduralTrackModel(model, options = {}) {
82
+ const length = options.length ?? {};
83
+ const validation = options.validation ?? {};
84
+ return (
85
+ model.length >= (length.min ?? GENERATED_TRACK_MIN_LENGTH) &&
86
+ model.length <= (length.max ?? GENERATED_TRACK_MAX_LENGTH) &&
87
+ hasEnoughShapeVariation(model.centerlineControls, validation.minShapeVariation ?? MIN_TRACK_SHAPE_VARIATION) &&
88
+ samplesStayInsideWorld(model.samples) &&
89
+ hasEnoughTrackClearance(
90
+ model.samples,
91
+ model.length,
92
+ model.width * (validation.minClearanceMultiplier ?? MIN_TRACK_CLEARANCE_MULTIPLIER),
93
+ validation.minNonAdjacentArcDistance ?? MIN_NON_ADJACENT_ARC_DISTANCE,
94
+ ) &&
95
+ hasReasonableTurnSharpness(model.samples, validation) &&
96
+ !hasSelfIntersections(model.samples)
97
+ );
98
+ }