@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
@@ -1,23 +1,49 @@
1
1
  import { lerp, normalizeAngle } from '../simulation/simMath.js';
2
2
 
3
+ const CAR_BUFFER = Symbol('renderSnapshotCars');
4
+ const GHOST_BUFFER = Symbol('renderSnapshotGhosts');
5
+ const SAFETY_CAR_BUFFER = Symbol('renderSnapshotSafetyCar');
6
+
3
7
  function interpolateAngle(previous, current, amount) {
4
8
  if (!Number.isFinite(previous)) return current;
5
9
  return previous + normalizeAngle(current - previous) * amount;
6
10
  }
7
11
 
8
- function interpolateMovingEntity(entity, alpha) {
9
- return {
10
- ...entity,
11
- x: lerp(entity.previousX ?? entity.x, entity.x, alpha),
12
- y: lerp(entity.previousY ?? entity.y, entity.y, alpha),
13
- heading: interpolateAngle(entity.previousHeading ?? entity.heading, entity.heading, alpha),
14
- };
12
+ function interpolateMovingEntityInto(target, entity, alpha) {
13
+ Object.assign(target, entity);
14
+ target.x = lerp(entity.previousX ?? entity.x, entity.x, alpha);
15
+ target.y = lerp(entity.previousY ?? entity.y, entity.y, alpha);
16
+ target.heading = interpolateAngle(entity.previousHeading ?? entity.heading, entity.heading, alpha);
17
+ return target;
18
+ }
19
+
20
+ function interpolateEntityListInto(sourceEntities = [], targetEntities = [], alpha) {
21
+ targetEntities.length = sourceEntities.length;
22
+ for (let index = 0; index < sourceEntities.length; index += 1) {
23
+ const source = sourceEntities[index];
24
+ const target = targetEntities[index] ?? {};
25
+ targetEntities[index] = interpolateMovingEntityInto(target, source, alpha);
26
+ }
27
+ return targetEntities;
28
+ }
29
+
30
+ export function interpolateRenderSnapshotInto(target, snapshot, alpha) {
31
+ if (!target || typeof target !== 'object') target = {};
32
+ const carBuffer = target[CAR_BUFFER] ?? [];
33
+ const ghostBuffer = target[GHOST_BUFFER] ?? [];
34
+ const safetyCarBuffer = target[SAFETY_CAR_BUFFER] ?? {};
35
+ target[CAR_BUFFER] = carBuffer;
36
+ target[GHOST_BUFFER] = ghostBuffer;
37
+ target[SAFETY_CAR_BUFFER] = safetyCarBuffer;
38
+ Object.assign(target, snapshot);
39
+ target.cars = interpolateEntityListInto(snapshot.cars ?? [], carBuffer, alpha);
40
+ target.replayGhosts = interpolateEntityListInto(snapshot.replayGhosts ?? [], ghostBuffer, alpha);
41
+ target.safetyCar = snapshot.safetyCar
42
+ ? interpolateMovingEntityInto(safetyCarBuffer, snapshot.safetyCar, alpha)
43
+ : snapshot.safetyCar;
44
+ return target;
15
45
  }
16
46
 
17
47
  export function createRenderSnapshot(snapshot, alpha) {
18
- return {
19
- ...snapshot,
20
- cars: snapshot.cars.map((car) => interpolateMovingEntity(car, alpha)),
21
- safetyCar: interpolateMovingEntity(snapshot.safetyCar, alpha),
22
- };
48
+ return interpolateRenderSnapshotInto({}, snapshot, alpha);
23
49
  }
@@ -0,0 +1,34 @@
1
+ import { Graphics } from 'pixi.js';
2
+ import { offsetTrackPoint, pointAt } from '../../simulation/trackModel.js';
3
+ import { FINISH_LINE_COLUMNS, FINISH_LINE_DEPTH } from './trackRenderConstants.js';
4
+
5
+ export function addFinishLine(asset, track) {
6
+ const finishLine = new Graphics();
7
+ const halfDepth = FINISH_LINE_DEPTH / 2;
8
+ addFinishLineHalf(finishLine, track, -halfDepth, 0, 0);
9
+ addFinishLineHalf(finishLine, track, 0, halfDepth, 1);
10
+ asset.container.addChild(finishLine);
11
+
12
+ }
13
+
14
+ export function addFinishLineHalf(graphics, track, startDistance, endDistance, rowOffset) {
15
+ const start = pointAt(track, startDistance);
16
+ const end = pointAt(track, endDistance);
17
+ const roadPadding = 10;
18
+ const width = track.width - roadPadding * 2;
19
+ const leftEdge = -width / 2;
20
+ const cellWidth = width / FINISH_LINE_COLUMNS;
21
+
22
+ for (let column = 0; column < FINISH_LINE_COLUMNS; column += 1) {
23
+ const innerOffset = leftEdge + column * cellWidth;
24
+ const outerOffset = innerOffset + cellWidth;
25
+ const color = (column + rowOffset) % 2 === 0 ? 0xf8fafc : 0x0b0d12;
26
+ const a = offsetTrackPoint(start, innerOffset);
27
+ const b = offsetTrackPoint(start, outerOffset);
28
+ const c = offsetTrackPoint(end, outerOffset);
29
+ const d = offsetTrackPoint(end, innerOffset);
30
+
31
+ graphics.poly([a.x, a.y, b.x, b.y, c.x, c.y, d.x, d.y]).fill(color);
32
+ }
33
+
34
+ }
@@ -0,0 +1,47 @@
1
+ import { Graphics } from 'pixi.js';
2
+ import { offsetTrackPoint, pointAt } from '../../simulation/trackModel.js';
3
+ import { START_GRID_BOX_LENGTH, START_GRID_BOX_WIDTH, START_GRID_FIRST_DISTANCE, START_GRID_LATERAL_OFFSET, START_GRID_SLOT_COUNT, START_GRID_SLOT_SPACING } from './trackRenderConstants.js';
4
+
5
+ export function addStartingGrid(asset, track) {
6
+ const grid = new Graphics();
7
+
8
+ for (let index = 0; index < START_GRID_SLOT_COUNT; index += 1) {
9
+ const base = pointAt(track, START_GRID_FIRST_DISTANCE - index * START_GRID_SLOT_SPACING);
10
+ const lateralOffset = index % 2 === 0 ? -START_GRID_LATERAL_OFFSET : START_GRID_LATERAL_OFFSET;
11
+ const center = offsetTrackPoint(base, lateralOffset);
12
+ const forwardX = Math.cos(base.heading);
13
+ const forwardY = Math.sin(base.heading);
14
+ const normalX = base.normalX;
15
+ const normalY = base.normalY;
16
+ const halfLength = START_GRID_BOX_LENGTH / 2;
17
+ const halfWidth = START_GRID_BOX_WIDTH / 2;
18
+ const corners = [
19
+ {
20
+ x: center.x + forwardX * halfLength + normalX * halfWidth,
21
+ y: center.y + forwardY * halfLength + normalY * halfWidth,
22
+ },
23
+ {
24
+ x: center.x + forwardX * halfLength - normalX * halfWidth,
25
+ y: center.y + forwardY * halfLength - normalY * halfWidth,
26
+ },
27
+ {
28
+ x: center.x - forwardX * halfLength - normalX * halfWidth,
29
+ y: center.y - forwardY * halfLength - normalY * halfWidth,
30
+ },
31
+ {
32
+ x: center.x - forwardX * halfLength + normalX * halfWidth,
33
+ y: center.y - forwardY * halfLength + normalY * halfWidth,
34
+ },
35
+ ];
36
+
37
+ grid.poly(corners.flatMap((corner) => [corner.x, corner.y])).stroke({
38
+ width: 3,
39
+ color: 0xf8fafc,
40
+ alpha: 0.8,
41
+ join: 'round',
42
+ });
43
+ }
44
+
45
+ asset.container.addChild(grid);
46
+
47
+ }
@@ -0,0 +1,136 @@
1
+ import { nearestTrackState, offsetTrackPoint } from '../../simulation/trackModel.js';
2
+ import { queryNearbyTrackProjections } from '../../simulation/track/trackQueryIndex.js';
3
+ import { metersToSimUnits } from '../../simulation/units.js';
4
+ import { NON_LOCAL_SAMPLE_STEP, OFFSET_GAP_SAMPLE_COUNT, OFFSET_SEGMENT_SAMPLE_COUNT } from './trackRenderConstants.js';
5
+ import { arcDistance, interpolatedSegmentPoint, pointDistance } from './trackRenderGeometry.js';
6
+
7
+ export function offsetPointIsLocal(track, source, point, offset) {
8
+ const state = nearestTrackState(track, point);
9
+ const localTolerance = Math.max(metersToSimUnits(105), Math.abs(offset) * 2.1);
10
+ const minimumEdgeDistance = Math.min(track.width / 2 - metersToSimUnits(4), Math.abs(offset) * 0.72);
11
+
12
+ return (
13
+ arcDistance(track, state.distance, source.distance) <= localTolerance &&
14
+ state.crossTrackError >= minimumEdgeDistance
15
+ );
16
+ }
17
+
18
+ export function offsetPointOverlapsNonLocalRoad(track, source, point, offset) {
19
+ const localTolerance = Math.max(metersToSimUnits(105), Math.abs(offset) * 2.1);
20
+ const roadBand = track.width / 2 + (track.kerbWidth ?? 0) + metersToSimUnits(4);
21
+ const roadBandSquared = (roadBand + metersToSimUnits(20)) ** 2;
22
+ const indexedCandidates = queryNearbyTrackProjections(track, point);
23
+
24
+ if (indexedCandidates) {
25
+ return indexedCandidates.some((state) => (
26
+ arcDistance(track, state.distance, source.distance) > localTolerance &&
27
+ state.distanceSquared <= roadBandSquared &&
28
+ state.crossTrackError <= roadBand
29
+ ));
30
+ }
31
+
32
+ for (let index = 0; index < track.samples.length - 1; index += NON_LOCAL_SAMPLE_STEP) {
33
+ const sample = track.samples[index];
34
+ if (arcDistance(track, sample.distance, source.distance) <= localTolerance) continue;
35
+
36
+ const dx = point.x - sample.x;
37
+ const dy = point.y - sample.y;
38
+ if (dx * dx + dy * dy > roadBandSquared) continue;
39
+
40
+ const signedOffset = dx * sample.normalX + dy * sample.normalY;
41
+ if (Math.abs(signedOffset) <= roadBand) return true;
42
+ }
43
+
44
+ return false;
45
+ }
46
+
47
+ export function offsetSegmentIsSafe(track, current, next, start, end, offset) {
48
+ const centerDistance = Math.hypot(next.x - current.x, next.y - current.y);
49
+ const edgeDistance = Math.hypot(end.x - start.x, end.y - start.y);
50
+ const maxEdgeDistance = Math.max(centerDistance * 2.6, track.width * 0.58);
51
+ if (edgeDistance > maxEdgeDistance || Math.abs(offset) > track.width + track.gravelWidth + track.runoffWidth) {
52
+ return false;
53
+ }
54
+
55
+ const forwardDistance = ((next.distance - current.distance + track.length) % track.length) || centerDistance;
56
+
57
+ for (let sample = 0; sample <= OFFSET_SEGMENT_SAMPLE_COUNT; sample += 1) {
58
+ const amount = sample / OFFSET_SEGMENT_SAMPLE_COUNT;
59
+ const source = {
60
+ distance: (current.distance + forwardDistance * amount) % track.length,
61
+ };
62
+ const point = interpolatedSegmentPoint(start, end, amount);
63
+ if (!offsetPointIsLocal(track, source, point, offset)) return false;
64
+ if (offsetPointOverlapsNonLocalRoad(track, source, point, offset)) return false;
65
+ }
66
+
67
+ return true;
68
+ }
69
+
70
+ export function offsetGapBridgeIsSafe(track, start, end, width) {
71
+ const bridgeDistance = pointDistance(start, end);
72
+ const maximumBridgeDistance = Math.max(track.width * 0.08, width * 3.4);
73
+ if (bridgeDistance > maximumBridgeDistance) return false;
74
+
75
+ const minimumEdgeDistance = track.width / 2 - Math.max(width * 2.5, metersToSimUnits(4.75));
76
+ for (let sample = 1; sample < OFFSET_GAP_SAMPLE_COUNT; sample += 1) {
77
+ const amount = sample / OFFSET_GAP_SAMPLE_COUNT;
78
+ const point = interpolatedSegmentPoint(start, end, amount);
79
+ const state = nearestTrackState(track, point);
80
+ if (state.crossTrackError < minimumEdgeDistance) return false;
81
+ }
82
+
83
+ return true;
84
+ }
85
+
86
+ export function getOffsetStrokeSegments(track, {
87
+ side,
88
+ offset,
89
+ step = SEGMENTED_STROKE_STEP,
90
+ }) {
91
+ const samples = track.samples.slice(0, -1);
92
+ const signedOffset = side * offset;
93
+
94
+ return samples.map((current, index) => {
95
+ if (index % step !== 0) return null;
96
+
97
+ const next = samples[(index + step) % samples.length];
98
+ const start = offsetTrackPoint(current, signedOffset);
99
+ const end = offsetTrackPoint(next, signedOffset);
100
+
101
+ return {
102
+ current,
103
+ next,
104
+ start,
105
+ end,
106
+ safe: offsetSegmentIsSafe(track, current, next, start, end, signedOffset),
107
+ };
108
+ }).filter(Boolean);
109
+ }
110
+
111
+ export function getOffsetGapBridges(track, segments, width) {
112
+ if (segments.length < 2) return [];
113
+
114
+ return segments.flatMap((segment, index) => {
115
+ if (!segment.safe) return [];
116
+
117
+ const previousIndex = (index - 1 + segments.length) % segments.length;
118
+ if (segments[previousIndex].safe) return [];
119
+
120
+ let previousSafe = null;
121
+ for (let lookup = 2; lookup <= segments.length; lookup += 1) {
122
+ const candidate = segments[(index - lookup + segments.length) % segments.length];
123
+ if (!candidate.safe) continue;
124
+ previousSafe = candidate;
125
+ break;
126
+ }
127
+
128
+ if (!previousSafe || previousSafe === segment) return [];
129
+ if (!offsetGapBridgeIsSafe(track, previousSafe.end, segment.start, width)) return [];
130
+
131
+ return [{
132
+ start: previousSafe.end,
133
+ end: segment.start,
134
+ }];
135
+ });
136
+ }
@@ -0,0 +1,248 @@
1
+ import { Graphics } from 'pixi.js';
2
+ import { nearestTrackState } from '../../simulation/trackModel.js';
3
+ import { ASPHALT_COLOR, GRAVEL_COLOR, PIT_ASPHALT_COLOR, PIT_BOX_COLOR, PIT_CONNECTOR_WIDTH, PIT_LINE_COLOR, PIT_SPEED_LINE_COLOR } from './trackRenderConstants.js';
4
+ import { colorToNumber, createTeamPitGroupCorners, drawPolyline, offsetVectorPoint } from './trackRenderGeometry.js';
5
+
6
+ export function drawMainPitLaneOffsetLine(graphics, pitLane, offset, {
7
+ width,
8
+ color,
9
+ alpha = 1,
10
+ }) {
11
+ const start = offsetVectorPoint(pitLane.mainLane.start, pitLane.serviceNormal, offset);
12
+ const end = offsetVectorPoint(pitLane.mainLane.end, pitLane.serviceNormal, offset);
13
+ drawPolyline(graphics, [start, end], {
14
+ width,
15
+ color,
16
+ alpha,
17
+ cap: 'butt',
18
+ });
19
+ }
20
+
21
+ export function drawPitRoad(graphics, points, {
22
+ roadWidth,
23
+ asphaltColor = ASPHALT_COLOR,
24
+ edgeAlpha = 0.54,
25
+ cap = 'butt',
26
+ }) {
27
+ drawPolyline(graphics, points, {
28
+ width: roadWidth + 8,
29
+ color: 0x080a0f,
30
+ alpha: edgeAlpha,
31
+ cap,
32
+ });
33
+ drawPolyline(graphics, points, {
34
+ width: roadWidth,
35
+ color: asphaltColor,
36
+ alpha: 1,
37
+ cap,
38
+ });
39
+ }
40
+
41
+ export function drawPitRunoff(graphics, points, roadWidth) {
42
+ drawPolyline(graphics, points, {
43
+ width: roadWidth + 92,
44
+ color: GRAVEL_COLOR,
45
+ alpha: 1,
46
+ cap: 'butt',
47
+ });
48
+ }
49
+
50
+ export function getPitLaneRoadCenterlines(pitLane) {
51
+ return {
52
+ entryRoad: pitLane.entry.roadCenterline ?? [pitLane.entry.edgePoint, pitLane.mainLane.start],
53
+ exitRoad: pitLane.exit.roadCenterline ?? [pitLane.mainLane.end, pitLane.exit.edgePoint],
54
+ };
55
+ }
56
+
57
+ export function drawCurvedPitRoadEdges(graphics, points, roadWidth) {
58
+ if (!Array.isArray(points) || points.length < 2) return;
59
+ [-1, 1].forEach((side) => {
60
+ const edgePoints = points.map((point, index) => {
61
+ const before = points[Math.max(0, index - 1)];
62
+ const after = points[Math.min(points.length - 1, index + 1)];
63
+ const dx = after.x - before.x;
64
+ const dy = after.y - before.y;
65
+ const len = Math.hypot(dx, dy) || 1;
66
+ return {
67
+ x: point.x + (-dy / len) * side * roadWidth / 2,
68
+ y: point.y + (dx / len) * side * roadWidth / 2,
69
+ };
70
+ });
71
+ drawPolyline(graphics, edgePoints, {
72
+ width: 2,
73
+ color: PIT_LINE_COLOR,
74
+ alpha: 0.7,
75
+ cap: 'butt',
76
+ });
77
+ });
78
+ }
79
+
80
+ export function getPitRoadEdgePoints(track, points) {
81
+ if (!Array.isArray(points) || points.length < 2) return points;
82
+ const trackEdge = track.width / 2;
83
+ const firstVisible = points.findIndex((point) => nearestTrackState(track, point).crossTrackError >= trackEdge - 4);
84
+ const lastVisibleReverse = [...points]
85
+ .reverse()
86
+ .findIndex((point) => nearestTrackState(track, point).crossTrackError >= trackEdge - 4);
87
+
88
+ if (firstVisible < 0 || lastVisibleReverse < 0) return points;
89
+ const lastVisible = points.length - 1 - lastVisibleReverse;
90
+ return points.slice(firstVisible, lastVisible + 1);
91
+ }
92
+
93
+ export function addPitLaneRunoff(asset, track) {
94
+ if (!track.pitLane?.enabled) return;
95
+ const pitLane = track.pitLane;
96
+ const pitRunoff = new Graphics();
97
+ pitRunoff.label = 'pit-lane-runoff';
98
+
99
+ // Only the main pit lane gets a gravel shoulder; connectors run through the track's
100
+ // own gravel/runoff zone so they do not need a separate runoff stroke (which would
101
+ // bleed an undesirable gravel band into the track asphalt at the entry/exit endpoints).
102
+ const workingLaneExtent = pitLane.workingLane
103
+ ? pitLane.workingLane.offset + pitLane.workingLane.width / 2
104
+ : pitLane.width / 2;
105
+ drawPitRunoff(pitRunoff, pitLane.mainLane.points, Math.max(pitLane.width, workingLaneExtent * 2));
106
+
107
+ asset.container.addChild(pitRunoff);
108
+
109
+ }
110
+
111
+ export function addPitLane(asset, track) {
112
+ if (!track.pitLane?.enabled) return;
113
+ const pitLane = track.pitLane;
114
+ const pit = new Graphics();
115
+ pit.label = 'pit-lane';
116
+ const { entryRoad, exitRoad } = getPitLaneRoadCenterlines(pitLane);
117
+
118
+ drawPitRoad(pit, entryRoad, {
119
+ roadWidth: PIT_CONNECTOR_WIDTH,
120
+ asphaltColor: ASPHALT_COLOR,
121
+ edgeAlpha: 0.18,
122
+ cap: 'round',
123
+ });
124
+ drawPitRoad(pit, exitRoad, {
125
+ roadWidth: PIT_CONNECTOR_WIDTH,
126
+ asphaltColor: ASPHALT_COLOR,
127
+ edgeAlpha: 0.18,
128
+ cap: 'round',
129
+ });
130
+ drawPitRoad(pit, pitLane.mainLane.points, {
131
+ roadWidth: pitLane.width,
132
+ asphaltColor: PIT_ASPHALT_COLOR,
133
+ edgeAlpha: 0.62,
134
+ });
135
+ if (pitLane.workingLane?.points?.length >= 2) {
136
+ drawPitRoad(pit, pitLane.workingLane.points, {
137
+ roadWidth: pitLane.workingLane.width,
138
+ asphaltColor: PIT_ASPHALT_COLOR,
139
+ edgeAlpha: 0.52,
140
+ });
141
+ }
142
+
143
+ drawMainPitLaneOffsetLine(pit, pitLane, -pitLane.width / 2, {
144
+ width: 3,
145
+ color: PIT_SPEED_LINE_COLOR,
146
+ alpha: 0.9,
147
+ });
148
+ drawMainPitLaneOffsetLine(pit, pitLane, pitLane.width / 2, {
149
+ width: 3,
150
+ color: PIT_LINE_COLOR,
151
+ alpha: 0.84,
152
+ });
153
+ if (pitLane.workingLane) {
154
+ drawMainPitLaneOffsetLine(pit, pitLane, pitLane.workingLane.offset - pitLane.workingLane.width / 2, {
155
+ width: 2,
156
+ color: PIT_LINE_COLOR,
157
+ alpha: 0.48,
158
+ });
159
+ drawMainPitLaneOffsetLine(pit, pitLane, pitLane.workingLane.offset + pitLane.workingLane.width / 2, {
160
+ width: 3,
161
+ color: PIT_LINE_COLOR,
162
+ alpha: 0.78,
163
+ });
164
+ }
165
+
166
+ const heading = pitLane.mainLane.heading;
167
+ const laneNormal = { x: -Math.sin(heading), y: Math.cos(heading) };
168
+ [pitLane.mainLane.start, pitLane.mainLane.end].forEach((point) => {
169
+ const a = offsetVectorPoint(point, laneNormal, -pitLane.width / 2);
170
+ const b = offsetVectorPoint(point, laneNormal, pitLane.width / 2);
171
+ drawPolyline(pit, [a, b], {
172
+ width: 4,
173
+ color: PIT_SPEED_LINE_COLOR,
174
+ alpha: 0.95,
175
+ cap: 'butt',
176
+ });
177
+ });
178
+
179
+ [entryRoad, exitRoad].forEach((road) => {
180
+ drawCurvedPitRoadEdges(pit, getPitRoadEdgePoints(track, road), PIT_CONNECTOR_WIDTH);
181
+ });
182
+
183
+ (pitLane.teams ?? []).forEach((team) => {
184
+ const boxes = pitLane.boxes.filter((box) => team.boxIds?.includes(box.id));
185
+ const corners = createTeamPitGroupCorners(boxes);
186
+ if (corners.length < 4) return;
187
+ const color = colorToNumber(team.color);
188
+ pit.poly(corners.flatMap((corner) => [corner.x, corner.y])).fill({
189
+ color,
190
+ alpha: 0.24,
191
+ });
192
+ pit.poly(corners.flatMap((corner) => [corner.x, corner.y])).stroke({
193
+ width: 3,
194
+ color,
195
+ alpha: 0.72,
196
+ join: 'round',
197
+ });
198
+ });
199
+
200
+ (pitLane.serviceAreas ?? []).forEach((serviceArea) => {
201
+ const teamColor = colorToNumber(serviceArea.teamColor, PIT_SPEED_LINE_COLOR);
202
+ if (serviceArea.queueCorners?.length >= 4) {
203
+ pit.poly(serviceArea.queueCorners.flatMap((corner) => [corner.x, corner.y])).fill({
204
+ color: teamColor,
205
+ alpha: 0.13,
206
+ });
207
+ pit.poly(serviceArea.queueCorners.flatMap((corner) => [corner.x, corner.y])).stroke({
208
+ width: 2,
209
+ color: teamColor,
210
+ alpha: 0.44,
211
+ join: 'round',
212
+ });
213
+ }
214
+ pit.poly(serviceArea.corners.flatMap((corner) => [corner.x, corner.y])).fill({
215
+ color: teamColor,
216
+ alpha: 0.2,
217
+ });
218
+ pit.poly(serviceArea.corners.flatMap((corner) => [corner.x, corner.y])).stroke({
219
+ width: 3,
220
+ color: teamColor,
221
+ alpha: 0.92,
222
+ join: 'round',
223
+ });
224
+ });
225
+
226
+ pitLane.boxes.forEach((box) => {
227
+ const teamColor = colorToNumber(box.teamColor, box.teamBoxIndex === 0 ? PIT_LINE_COLOR : PIT_SPEED_LINE_COLOR);
228
+ pit.poly(box.corners.flatMap((corner) => [corner.x, corner.y])).fill({
229
+ color: PIT_BOX_COLOR,
230
+ alpha: 0.96,
231
+ });
232
+ pit.poly(box.corners.flatMap((corner) => [corner.x, corner.y])).stroke({
233
+ width: 2,
234
+ color: teamColor,
235
+ alpha: 0.9,
236
+ join: 'round',
237
+ });
238
+ drawPolyline(pit, [box.laneTarget, box.center], {
239
+ width: 2,
240
+ color: teamColor,
241
+ alpha: 0.28,
242
+ cap: 'butt',
243
+ });
244
+ });
245
+
246
+ asset.container.addChild(pit);
247
+
248
+ }