@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
@@ -0,0 +1,93 @@
1
+ export class HostLifecycleEmitter {
2
+ constructor(getOptions, getDispatchSource = () => undefined) {
3
+ this.getOptions = getOptions;
4
+ this.getDispatchSource = getDispatchSource;
5
+ this.reset();
6
+ }
7
+
8
+ reset() {
9
+ this.dispatchGeneration = (this.dispatchGeneration ?? 0) + 1;
10
+ this.lastLeaderLap = null;
11
+ this.emittedRaceEventKeys = new Set();
12
+ this.raceFinishEmitted = false;
13
+ }
14
+
15
+ captureDispatch() {
16
+ return { generation: this.dispatchGeneration, source: this.getDispatchSource() };
17
+ }
18
+
19
+ isDispatchCurrent(dispatch) {
20
+ return dispatch.source !== false &&
21
+ dispatch.generation === this.dispatchGeneration &&
22
+ dispatch.source === this.getDispatchSource();
23
+ }
24
+
25
+ emitCallback(name, ...args) {
26
+ const options = this.getOptions?.();
27
+ const callback = options?.[name];
28
+ if (typeof callback !== 'function') return;
29
+ try {
30
+ callback(...args);
31
+ } catch (error) {
32
+ if (name !== 'onError' && typeof options?.onError === 'function') {
33
+ try {
34
+ options.onError(error, { callback: name });
35
+ } catch {
36
+ // Host callbacks should not break the simulator runtime.
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ emitSnapshot(snapshot) {
43
+ const dispatch = this.captureDispatch();
44
+ if (!this.isDispatchCurrent(dispatch)) return;
45
+ const leader = snapshot.cars[0];
46
+ const leaderLap = leader?.lap;
47
+ if (Number.isFinite(leaderLap)) {
48
+ if (this.lastLeaderLap != null && leaderLap !== this.lastLeaderLap) {
49
+ this.emitCallback('onLapChange', {
50
+ previousLeaderLap: this.lastLeaderLap,
51
+ leaderLap,
52
+ leader,
53
+ snapshot,
54
+ });
55
+ if (!this.isDispatchCurrent(dispatch)) return;
56
+ }
57
+ this.lastLeaderLap = leaderLap;
58
+ }
59
+
60
+ this.emitRaceEvents(snapshot.events, snapshot, dispatch);
61
+ if (!this.isDispatchCurrent(dispatch)) return;
62
+
63
+ if (snapshot.raceControl.finished && !this.raceFinishEmitted) {
64
+ this.raceFinishEmitted = true;
65
+ this.emitCallback('onRaceFinish', {
66
+ winner: snapshot.raceControl.winner,
67
+ classification: snapshot.raceControl.classification ?? [],
68
+ snapshot,
69
+ });
70
+ }
71
+ }
72
+
73
+ emitRaceEvents(events = [], snapshot, dispatch = this.captureDispatch()) {
74
+ for (const event of events) {
75
+ if (!this.isDispatchCurrent(dispatch)) return;
76
+ const key = [
77
+ event.type,
78
+ event.at ?? snapshot?.time,
79
+ event.id ?? '',
80
+ event.penaltyId ?? '',
81
+ event.driverId ?? '',
82
+ event.carId ?? '',
83
+ event.otherCarId ?? '',
84
+ event.winnerId ?? '',
85
+ event.sequence ?? '',
86
+ event.status ?? '',
87
+ ].join(':');
88
+ if (this.emittedRaceEventKeys.has(key)) continue;
89
+ this.emittedRaceEventKeys.add(key);
90
+ this.emitCallback('onRaceEvent', event, snapshot);
91
+ }
92
+ }
93
+ }
@@ -52,7 +52,7 @@ export function shouldAutoHideActiveRaceData({ activeRaceDataId, options, readou
52
52
  );
53
53
  }
54
54
 
55
- export function renderRaceData({ car, drivers, readouts, options }) {
55
+ export function renderRaceData({ car, drivers, readouts, options, presentation = null }) {
56
56
  const panels = getRaceDataPanelBindings(readouts);
57
57
  if (!car || !panels.length) return false;
58
58
  if (typeof options.isRaceDataBannerEnabled === 'function' && !options.isRaceDataBannerEnabled('project')) return false;
@@ -66,11 +66,16 @@ export function renderRaceData({ car, drivers, readouts, options }) {
66
66
  binding.panel.classList.remove('is-radio-mode');
67
67
  clearRaceDataIdleMode(binding.panel);
68
68
  setText(binding.kicker, 'Project');
69
- setText(binding.title, driver.name);
69
+ setText(binding.title, presentation?.name ?? driver.name);
70
70
  setText(binding.number, formatDriverNumber(car.driverNumber ?? driver.driverNumber));
71
- setText(binding.subtitle, `${car.code} - P${car.rank} - ${driver.raceData?.[0] ?? 'Project entry'}`);
71
+ setText(binding.subtitle, `${car.code} - P${car.rank} - ${presentation?.summary ?? driver.raceData?.[0] ?? 'Project entry'}`);
72
72
  if (binding.open) {
73
- binding.open.hidden = typeof options.onDriverOpen !== 'function';
73
+ if (binding.open.tagName === 'A') {
74
+ binding.open.hidden = !presentation?.href;
75
+ if (presentation?.href) binding.open.setAttribute('href', presentation.href);
76
+ else binding.open.removeAttribute('href');
77
+ setText(binding.open, presentation?.linkLabel ?? 'Open project');
78
+ } else binding.open.hidden = typeof options.onDriverOpen !== 'function';
74
79
  }
75
80
  });
76
81
  return true;
@@ -1,5 +1,5 @@
1
1
  import { metersToSimUnits } from '../../simulation/units.js';
2
- import { VEHICLE_GEOMETRY } from '../../simulation/vehicleGeometry.js';
2
+ import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
3
3
 
4
4
  export const CAMERA_TARGET_LERP = 0.12;
5
5
  export const CAMERA_SCALE_LERP = 0.12;
@@ -1,4 +1,4 @@
1
- import { WORLD } from '../../simulation/trackModel.js';
1
+ import { WORLD } from '../../simulation/track/trackModel.js';
2
2
  import { clamp, normalizeAngle } from '../../simulation/simMath.js';
3
3
  import {
4
4
  CAMERA_PRESETS,
@@ -274,14 +274,7 @@ export class CameraController {
274
274
  return leader ? { x: leader.x, y: leader.y } : { x: WORLD.width / 2, y: WORLD.height / 2 };
275
275
  }
276
276
 
277
- getBaseScale() {
278
- const width = this.canvasHost.clientWidth || 900;
279
- const height = this.canvasHost.clientHeight || 640;
280
- const safeArea = this.getSafeArea(width);
281
- return Math.min(safeArea.width / (WORLD.width + 260), height / (WORLD.height + 220));
282
- }
283
-
284
- applyToWorldLayer(worldLayer, snapshot, { immediate = false, selectedId = null } = {}) {
277
+ applyToWorldLayer(worldLayer, snapshot, { immediate = false, selectedId = null, elapsedMs = 1000 / 60 } = {}) {
285
278
  if (!worldLayer) return;
286
279
 
287
280
  const width = this.canvasHost.clientWidth || 900;
@@ -293,18 +286,22 @@ export class CameraController {
293
286
  const target = frame.target;
294
287
  const targetRotation = Number.isFinite(frame.rotation) ? frame.rotation : 0;
295
288
  const snapCamera = immediate || !this.camera.initialized;
289
+ const elapsedFrames = Math.max(0, elapsedMs) / (1000 / 60);
290
+ const targetLerp = 1 - (1 - CAMERA_TARGET_LERP) ** elapsedFrames;
291
+ const scaleLerp = 1 - (1 - CAMERA_SCALE_LERP) ** elapsedFrames;
292
+ const rotationLerp = 1 - (1 - CAMERA_ROTATION_LERP) ** elapsedFrames;
296
293
  this.camera.x = snapCamera
297
294
  ? target.x
298
- : this.camera.x + (target.x - this.camera.x) * CAMERA_TARGET_LERP;
295
+ : this.camera.x + (target.x - this.camera.x) * targetLerp;
299
296
  this.camera.y = snapCamera
300
297
  ? target.y
301
- : this.camera.y + (target.y - this.camera.y) * CAMERA_TARGET_LERP;
298
+ : this.camera.y + (target.y - this.camera.y) * targetLerp;
302
299
  const activeScale = snapCamera || this.camera.scale === null
303
300
  ? scale
304
- : this.camera.scale + (scale - this.camera.scale) * CAMERA_SCALE_LERP;
301
+ : this.camera.scale + (scale - this.camera.scale) * scaleLerp;
305
302
  const activeRotation = snapCamera
306
303
  ? targetRotation
307
- : this.camera.rotation + normalizeAngle(targetRotation - this.camera.rotation) * CAMERA_ROTATION_LERP;
304
+ : this.camera.rotation + normalizeAngle(targetRotation - this.camera.rotation) * rotationLerp;
308
305
  this.camera.initialized = true;
309
306
  this.camera.scale = activeScale;
310
307
  this.camera.rotation = activeRotation;
@@ -1,4 +1,4 @@
1
- import { WORLD } from '../../simulation/trackModel.js';
1
+ import { WORLD } from '../../simulation/track/trackModel.js';
2
2
  import { clamp } from '../../simulation/simMath.js';
3
3
  import {
4
4
  CAMERA_MAX_ZOOM,
@@ -0,0 +1,45 @@
1
+ // Separately mounted controls share one runtime view; they never own race state.
2
+ export class SimulatorViewControls {
3
+ constructor(root, onView, signal) {
4
+ this.surfaces = [...root.querySelectorAll('[data-paddock-component="view-controls"]')];
5
+ this.buttons = this.surfaces.flatMap((surface) => [...surface.querySelectorAll('[data-grid-view]')]);
6
+ this.toggles = this.surfaces.flatMap((surface) => [...surface.querySelectorAll('[data-grid-toggle]')]);
7
+ this.view = this.buttons.find((button) => button.getAttribute('aria-pressed') === 'true')?.dataset.gridView ?? 'race';
8
+ for (const button of this.buttons) {
9
+ button.addEventListener('click', () => onView(button.dataset.gridView), { signal });
10
+ button.disabled = false;
11
+ }
12
+ for (const button of this.toggles) {
13
+ button.addEventListener('click', () => onView(this.view === 'race' ? 'grid-walk' : 'race'), { signal });
14
+ button.disabled = false;
15
+ }
16
+ for (const surface of this.surfaces) surface.hidden = false;
17
+ }
18
+
19
+ update(view, variant = 'switch') {
20
+ this.view = view;
21
+ const toggle = variant === 'toggle';
22
+ for (const button of this.buttons) {
23
+ button.hidden = toggle;
24
+ button.setAttribute('aria-pressed', String(button.dataset.gridView === view));
25
+ }
26
+ for (const button of this.toggles) {
27
+ button.hidden = !toggle;
28
+ button.dataset.gridAction = view === 'grid-walk' ? 'start' : 'stop';
29
+ const label = button.querySelector?.('[data-control-label]') ?? button;
30
+ label.textContent = view === 'grid-walk' ? 'Start race' : 'Stop race';
31
+ }
32
+ }
33
+
34
+ focusRace() {
35
+ const button = [...this.buttons.filter((node) => node.dataset.gridView === 'race'), ...this.toggles]
36
+ .find((node) => !node.hidden && node.checkVisibility());
37
+ button?.focus({ preventScroll: true });
38
+ return Boolean(button);
39
+ }
40
+
41
+ destroy() {
42
+ for (const button of [...this.buttons, ...this.toggles]) button.disabled = true;
43
+ for (const surface of this.surfaces) surface.hidden = true;
44
+ }
45
+ }
@@ -0,0 +1,64 @@
1
+ function uniqueNodes(nodes = []) {
2
+ return [...new Set([...nodes].filter(Boolean))];
3
+ }
4
+
5
+ // Bind DOM gestures to commands without owning race state or a simulation.
6
+ export function bindWorkbenchControls(nodes, commands, signal) {
7
+ const eventOptions = { signal };
8
+ nodes.safetyButtons.forEach((button) => {
9
+ button.addEventListener('click', commands.toggleSafetyCar, eventOptions);
10
+ });
11
+ uniqueNodes(nodes.restartButtons?.length ? nodes.restartButtons : [nodes.restartButton]).forEach((button) => button.addEventListener('click', commands.restart, eventOptions));
12
+ nodes.cameraButtons.forEach((button) => {
13
+ button.addEventListener('click', () => commands.setCameraMode(button.dataset.cameraMode), eventOptions);
14
+ });
15
+ for (const [buttons, fallback, direction] of [[nodes.zoomInButtons, nodes.zoomInButton, 1], [nodes.zoomOutButtons, nodes.zoomOutButton, -1]]) {
16
+ uniqueNodes(buttons?.length ? buttons : [fallback]).forEach((button) => button.addEventListener('click', () => commands.adjustCameraZoom(direction), eventOptions));
17
+ }
18
+ nodes.simulationSpeedButtons?.forEach((button) => {
19
+ button.addEventListener('click', commands.cycleSimulationSpeed, eventOptions);
20
+ });
21
+ commands.updateSimulationSpeedControls();
22
+ nodes.bannerMuteButtons?.forEach((button) => {
23
+ button.addEventListener('click', commands.toggleRaceDataBanners, eventOptions);
24
+ });
25
+ nodes.overviewModeButtons.forEach((button) => {
26
+ button.addEventListener('click', () => {
27
+ commands.setOverviewMode(button.dataset.overviewMode === 'driver' ? 'driver' : 'vehicle');
28
+ }, eventOptions);
29
+ });
30
+ nodes.timingGapModeButtons.forEach((button) => {
31
+ button.addEventListener('click', commands.toggleTimingGapMode, eventOptions);
32
+ });
33
+ nodes.timingLists?.forEach?.((timingList) => {
34
+ timingList.addEventListener?.('click', (event) => {
35
+ const row = event.target instanceof Element ? event.target.closest('[data-driver-id]') : null;
36
+ if (row) commands.selectCar(row.dataset.driverId);
37
+ }, eventOptions);
38
+ });
39
+ nodes.timingPanelToggles?.forEach?.((toggle) => {
40
+ toggle.addEventListener?.('click', () => {
41
+ const racePanel = toggle.closest?.('.sim-canvas-panel');
42
+ commands.setTimingPanelOpen(!racePanel?.classList?.contains?.('is-timing-panel-open'), racePanel);
43
+ }, eventOptions);
44
+ });
45
+ const openButtons = uniqueNodes(nodes.raceDataOpens?.length ? nodes.raceDataOpens : [nodes.openButton]);
46
+ openButtons.forEach((button) => button.addEventListener('click', commands.openDriver, eventOptions));
47
+ const dismissButtons = uniqueNodes(nodes.raceDataDismisses?.length ? nodes.raceDataDismisses : [nodes.raceDataDismiss]);
48
+ dismissButtons.forEach((button) => button.addEventListener('click', (event) => {
49
+ event.preventDefault();
50
+ event.stopPropagation();
51
+ commands.dismissRaceDataPanel(performance.now());
52
+ }, eventOptions));
53
+ nodes.telemetryDrawerToggle?.addEventListener('click', commands.toggleTelemetryDrawer, eventOptions);
54
+ }
55
+
56
+ export function bindCameraWheelControls(canvasHost, adjustCameraZoom, eventOptions, isEnabled = () => true) {
57
+ if (!canvasHost?.addEventListener) return;
58
+ canvasHost.addEventListener('wheel', (event) => {
59
+ if (!isEnabled() || !event.cancelable || event.defaultPrevented || event.ctrlKey || event.metaKey ||
60
+ !Number.isFinite(event.deltaY) || event.deltaY === 0) return;
61
+ if (!adjustCameraZoom(event.deltaY > 0 ? -1 : 1)) return;
62
+ event.preventDefault?.();
63
+ }, { ...eventOptions, passive: false });
64
+ }
@@ -0,0 +1,41 @@
1
+ export function renderTelemetryDrawerDisclosure({ workbench, drawer, toggle }, open) {
2
+ workbench?.classList?.toggle?.('is-telemetry-open', open);
3
+ if (open) {
4
+ drawer?.removeAttribute?.('inert');
5
+ drawer?.setAttribute?.('aria-hidden', 'false');
6
+ } else {
7
+ drawer?.setAttribute?.('inert', '');
8
+ drawer?.setAttribute?.('aria-hidden', 'true');
9
+ }
10
+ toggle?.setAttribute?.('aria-expanded', String(open));
11
+ if (toggle) {
12
+ const label = toggle.querySelector?.('[data-control-label]') ?? toggle;
13
+ label.textContent = open ? 'Close' : 'Telemetry';
14
+ toggle.setAttribute('aria-label', open ? 'Close telemetry' : 'Open telemetry');
15
+ }
16
+ }
17
+
18
+ export function isTimingPanelDisclosureMode(toggle) {
19
+ if (!toggle) return false;
20
+ const style = getComputedStyle(toggle);
21
+ return style.display !== 'none' && style.visibility !== 'hidden';
22
+ }
23
+
24
+ export function syncTimingPanelDisclosureState({ toggles, targetPanel = null, isDisclosureMode = isTimingPanelDisclosureMode }) {
25
+ for (const toggle of [...(toggles ?? [])]) {
26
+ const racePanel = toggle.closest?.('.sim-canvas-panel');
27
+ if (targetPanel && racePanel !== targetPanel) continue;
28
+ const tower = racePanel?.querySelector?.('[data-timing-tower]');
29
+ const disclosureMode = isDisclosureMode(toggle);
30
+ const open = disclosureMode && Boolean(racePanel?.classList?.contains?.('is-timing-panel-open'));
31
+ racePanel?.classList?.toggle?.('is-timing-panel-open', open);
32
+ if (!disclosureMode || open) {
33
+ tower?.removeAttribute?.('inert');
34
+ tower?.setAttribute?.('aria-hidden', 'false');
35
+ } else {
36
+ tower?.setAttribute?.('inert', '');
37
+ tower?.setAttribute?.('aria-hidden', 'true');
38
+ }
39
+ toggle.setAttribute?.('aria-expanded', String(open));
40
+ }
41
+ }
@@ -13,7 +13,7 @@ export function resolveNodes(nodes, fallbackNode = null) {
13
13
  return resolved;
14
14
  }
15
15
 
16
- function queryRaceDataPanelBindings(root) {
16
+ export function queryRaceDataPanelBindings(root) {
17
17
  const panelSelector = '[data-race-data-panel]';
18
18
  const panels = toArray(root.querySelectorAll(panelSelector));
19
19
  const fallbackPanel = panels.length ? null : root.querySelector(panelSelector);
@@ -36,11 +36,6 @@ function queryRaceDataPanelBindings(root) {
36
36
  const fallbackDismiss = first.dismiss ?? root.querySelector('[data-race-data-dismiss]');
37
37
  return {
38
38
  raceDataPanelBindings: bindings,
39
- raceDataPanels,
40
- raceDataKickers: bindings.map((binding) => binding.kicker).filter(Boolean),
41
- raceDataTitles: bindings.map((binding) => binding.title).filter(Boolean),
42
- raceDataNumbers: bindings.map((binding) => binding.number).filter(Boolean),
43
- raceDataSubtitles: bindings.map((binding) => binding.subtitle).filter(Boolean),
44
39
  raceDataOpens: bindings.map((binding) => binding.open).filter(Boolean),
45
40
  raceDataDismisses: bindings.map((binding) => binding.dismiss).filter(Boolean),
46
41
  raceDataPanel: first.panel ?? null,
@@ -53,18 +48,27 @@ function queryRaceDataPanelBindings(root) {
53
48
  };
54
49
  }
55
50
 
56
- function queryCarOverviewFieldBindings(root) {
57
- const fields = toArray(root.querySelectorAll('[data-overview-field]'));
58
- return fields.map((field) => ({
59
- field,
60
- label: field.querySelector?.('[data-overview-field-label]') ?? null,
61
- value: field.querySelector?.('[data-overview-field-value]') ?? null,
51
+ export function queryCarOverviewBindings(root) {
52
+ return toArray(root.querySelectorAll('[data-paddock-component="car-driver-overview"]')).map((panel) => ({
53
+ carOverview: panel,
54
+ ...Object.fromEntries([
55
+ ['carOverviewTitle', '[data-car-overview-title]'], ['carOverviewDiagram', '.car-overview-diagram'],
56
+ ['carOverviewCode', '[data-car-overview-code]'], ['carOverviewIcon', '[data-car-overview-icon]'],
57
+ ['carOverviewImage', '[data-car-overview-image]'], ['carOverviewNumber', '[data-car-overview-number]'],
58
+ ['carOverviewCoreStat', '[data-car-overview-core-stat]'],
59
+ ].map(([name, selector]) => [name, panel.querySelector(selector)])),
60
+ carOverviewFieldBindings: toArray(panel.querySelectorAll('[data-overview-field]')).map((field) => ({
61
+ field, label: field.querySelector('[data-overview-field-label]'), value: field.querySelector('[data-overview-field-value]'),
62
+ })),
62
63
  }));
63
64
  }
64
65
 
65
66
  export function querySimulatorDom(root) {
66
67
  const raceDataBindings = queryRaceDataPanelBindings(root);
67
- const carOverviewFieldBindings = queryCarOverviewFieldBindings(root);
68
+ const carOverviewBindings = queryCarOverviewBindings(root);
69
+ const carOverviewFieldBindings = carOverviewBindings[0]?.carOverviewFieldBindings ?? toArray(root.querySelectorAll('[data-overview-field]')).map((field) => ({
70
+ field, label: field.querySelector?.('[data-overview-field-label]') ?? null, value: field.querySelector?.('[data-overview-field-value]') ?? null,
71
+ }));
68
72
  const readouts = {
69
73
  timingTower: root.querySelector('[data-timing-tower]'),
70
74
  timingTowers: root.querySelectorAll('[data-timing-tower]'),
@@ -123,6 +127,7 @@ export function querySimulatorDom(root) {
123
127
  carOverviewNumber: root.querySelector('[data-car-overview-number]'),
124
128
  carOverviewCoreStat: root.querySelector('[data-car-overview-core-stat]'),
125
129
  carOverviewFields: root.querySelectorAll('[data-overview-field]'),
130
+ carOverviewBindings,
126
131
  carOverviewFieldBindings,
127
132
  telemetryDrawerWorkbench: root.querySelector('[data-race-telemetry-drawer]'),
128
133
  telemetryDrawer: root.querySelector('[data-telemetry-drawer]'),
@@ -141,6 +146,7 @@ export function querySimulatorDom(root) {
141
146
  canvasHost: root.querySelector('[data-track-canvas]'),
142
147
  safetyButtons: root.querySelectorAll('[data-safety-car]'),
143
148
  restartButton: root.querySelector('[data-restart-race]'),
149
+ restartButtons: root.querySelectorAll('[data-restart-race]'),
144
150
  openButton: raceDataBindings.raceDataOpen,
145
151
  timingList: root.querySelector('[data-timing-list]'),
146
152
  timingGapModeButtons: root.querySelectorAll('[data-timing-gap-toggle]'),
@@ -149,7 +155,9 @@ export function querySimulatorDom(root) {
149
155
  bannerMuteButtons: root.querySelectorAll('[data-race-data-banners-muted]'),
150
156
  simulationSpeedButtons: root.querySelectorAll('[data-simulation-speed]'),
151
157
  zoomInButton: root.querySelector('[data-zoom-in]'),
158
+ zoomInButtons: root.querySelectorAll('[data-zoom-in]'),
152
159
  zoomOutButton: root.querySelector('[data-zoom-out]'),
160
+ zoomOutButtons: root.querySelectorAll('[data-zoom-out]'),
153
161
  timingLists: root.querySelectorAll('[data-timing-list]'),
154
162
  readouts,
155
163
  startLightNodes: [...(readouts.startLights?.querySelectorAll('.start-lights__gantry span') ?? [])],
@@ -0,0 +1,180 @@
1
+ import { createGridWalkMarkup } from '../../ui/gridWalkTemplate.js';
2
+ import { GridWalkSession } from './gridWalkSession.js';
3
+ import { GridWalkCamera, gridWalkOrientation } from './gridWalkCamera.js';
4
+ import { GridWalkCrew } from './gridWalkCrew.js';
5
+ import { GridWalkNavigation } from './gridWalkNavigation.js';
6
+ import { GridWalkControls } from './gridWalkControls.js';
7
+ import { GridWalkLabels } from './gridWalkLabels.js';
8
+ import { GridWalkProjectBanner } from './gridWalkProjectBanner.js';
9
+ import { GridWalkLayout } from './gridWalkLayout.js';
10
+ import { SimulatorViewControls } from '../controls/SimulatorViewControls.js';
11
+
12
+ // Coordinates lifecycle and scene presentation. Each UI/input component owns
13
+ // its own actions and geometry; none owns or steps the simulation.
14
+ export class GridWalkController {
15
+ constructor(app) {
16
+ this.app = app;
17
+ this.session = new GridWalkSession(app.sim, app.options.gridWalk);
18
+ this.abort = new AbortController();
19
+ this.elapsed = 0;
20
+ this.lastTime = performance.now();
21
+ this.panel = app.canvasHost.closest('.sim-canvas-panel');
22
+ const wrapper = app.canvasHost.ownerDocument.createElement('div');
23
+ const cars = this.session.snapshot.cars;
24
+ wrapper.innerHTML = createGridWalkMarkup(app.drivers, cars, app.options.ui);
25
+ this.element = wrapper.firstElementChild;
26
+ const select = (id) => app.selectCar(id, { focus: true });
27
+ const { signal } = this.abort;
28
+ this.viewControls = new SimulatorViewControls(app.root, (view) => this.setView(view), signal);
29
+ this.controls = new GridWalkControls(this.element, {
30
+ onSelect: select, onMove: (direction) => this.move(direction),
31
+ }, signal);
32
+ this.labels = new GridWalkLabels(this.element, cars, select, signal);
33
+ this.banner = new GridWalkProjectBanner(this.element, () => this.closeProjectBanner(), signal);
34
+ this.camera = new GridWalkCamera(cars);
35
+ this.layout = new GridWalkLayout({ element: this.element, panel: this.panel, canvasHost: app.canvasHost, root: app.root });
36
+ this.navigation = new GridWalkNavigation({ host: app.canvasHost, surface: this.panel,
37
+ isEnabled: () => this.active && !this.session.clearing,
38
+ getOrientation: () => this.orientation,
39
+ onPan: (delta) => {
40
+ if (!this.camera.pan(delta)) return false;
41
+ this.render();
42
+ return true;
43
+ },
44
+ onHome: () => { this.camera.focus(app.selectedId); this.render(); }, onSelect: select,
45
+ }, signal);
46
+ this.media = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)');
47
+ this.media?.addEventListener('change', () => {
48
+ this.session.advance(0, this.motion);
49
+ this.sync();
50
+ }, { signal });
51
+ this.crew = new GridWalkCrew(app.worldLayer, cars);
52
+ this.panel.appendChild(this.element);
53
+ }
54
+
55
+ get active() { return this.session.active; }
56
+ get motion() { return this.app.options.gridWalk.ambientMotion && !this.media?.matches; }
57
+ get needsAnimation() { return this.active && this.motion; }
58
+
59
+ setView(view) {
60
+ if (!this.session.setView(view, this.motion)) return false;
61
+ this.navigation.cancel();
62
+ // Preserve the fixed-step remainder; only discard time spent browsing.
63
+ this.app.lastTime = this.lastTime = performance.now();
64
+ if (view === 'race' && this.element.contains(this.element.ownerDocument.activeElement) && !this.viewControls.focusRace()) {
65
+ this.app.canvasHost.querySelector('canvas').focus({ preventScroll: true });
66
+ }
67
+ this.sync();
68
+ return true;
69
+ }
70
+
71
+ select(id, { focus = false } = {}) {
72
+ if (!this.active || this.session.clearing || !this.app.driverById.has(id)) return;
73
+ // A visible car stays where it was clicked. The selector can bring an
74
+ // offscreen entry into view without resetting the camera on every click.
75
+ if (!this.labels.isVisible(id)) this.camera.focus(id);
76
+ this.banner.open = true;
77
+ this.sync();
78
+ if (focus) {
79
+ if (!this.banner.element.hidden) this.banner.focus();
80
+ else this.labels.focus(id, this.controls.jump);
81
+ }
82
+ }
83
+
84
+ closeProjectBanner() {
85
+ this.banner.open = false;
86
+ this.sync();
87
+ this.labels.focus(this.app.selectedId, this.controls.jump);
88
+ }
89
+
90
+ move(direction) {
91
+ const cars = this.session.snapshot.cars;
92
+ const index = cars.findIndex((car) => car.id === this.app.selectedId);
93
+ this.app.selectCar(cars[(index + direction + cars.length) % cars.length].id, { focus: true });
94
+ }
95
+
96
+ sync({ updateTicker = true } = {}) {
97
+ const { app, session } = this;
98
+ this.navigation.sync();
99
+ this.panel.dataset.gridWalkActive = String(this.active);
100
+ this.element.dataset.view = session.view;
101
+ this.controls.update(session, app.selectedId);
102
+ this.viewControls.update(session.view, app.options.ui.viewControls);
103
+ this.element.querySelector('[data-grid-labels]').hidden = !this.active || session.clearing;
104
+ this.banner.update({ active: this.active, clearing: session.clearing,
105
+ enabled: app.isRaceDataBannerEnabled('project'), car: session.snapshot.cars.find((car) => car.id === app.selectedId),
106
+ drivers: app.drivers, options: app.options, presentation: app.driverById.get(app.selectedId).presentation });
107
+ for (const layer of [app.trailLayer, app.sensorLayer, app.pitLaneStatusLayer]) if (layer) layer.visible = !this.active;
108
+ this.syncLayout();
109
+ if (!this.active) app.syncTimingPanelDisclosureState();
110
+ if (this.active) this.render();
111
+ else {
112
+ this.crew.container.visible = false;
113
+ app.syncRendererToCurrentLayout({ render: true });
114
+ }
115
+ if (updateTicker) app.syncRuntimeTicker();
116
+ }
117
+
118
+ syncLayout() { this.layout.update(this.active); }
119
+
120
+ applyCamera() {
121
+ const { app } = this;
122
+ this.syncLayout();
123
+ const width = app.canvasHost.clientWidth;
124
+ const height = app.canvasHost.clientHeight;
125
+ if (!(width > 0 && height > 0)) return;
126
+ const orientation = gridWalkOrientation(width, height, this.orientation);
127
+ if (this.orientation && orientation !== this.orientation) this.navigation.cancel();
128
+ this.orientation = orientation;
129
+ this.element.dataset.orientation = this.orientation;
130
+ this.element.dataset.compactControls = String(width < 600 && height < 420);
131
+ const area = this.layout.measure(this.banner.measure(width, height));
132
+ this.element.dataset.compactLabels = String(area.bottom - area.top < 164);
133
+ this.frame = this.camera.update({ selectedId: app.selectedId, width, height, orientation: this.orientation, area });
134
+ this.crew.updateCamera(this.frame, width, height);
135
+ app.worldLayer.rotation = this.frame.rotation;
136
+ app.worldLayer.scale.set(this.frame.scale);
137
+ app.worldLayer.position.set(this.frame.x, this.frame.y);
138
+ this.labels.update(this.frame, app.selectedId, this.controls.jump);
139
+ }
140
+
141
+ render() {
142
+ const { app, session } = this;
143
+ const screen = app.app?.renderer?.screen;
144
+ if (!screen || screen.width !== Math.max(1, Math.round(app.canvasHost.clientWidth)) ||
145
+ screen.height !== Math.max(1, Math.round(app.canvasHost.clientHeight))) app.resizeRendererToCanvasHost();
146
+ this.applyCamera();
147
+ app.renderCars(session.snapshot);
148
+ this.crew.render({ active: this.active, motion: this.motion, elapsed: this.elapsed,
149
+ clearing: session.clearing, clearProgress: Math.min(1, session.clearElapsed / 650) });
150
+ app.app?.render?.();
151
+ }
152
+
153
+ tick(now) {
154
+ if (!this.active) return false;
155
+ const elapsed = Math.max(0, Math.min(now - this.lastTime, 100));
156
+ this.lastTime = now;
157
+ this.elapsed += elapsed;
158
+ const wasClearing = this.session.clearing;
159
+ this.session.advance(elapsed, this.motion);
160
+ if (wasClearing && !this.session.clearing) this.sync();
161
+ if (!this.active) return false;
162
+ // A busy browser can defer the media callback; stop on this frame too.
163
+ if (!this.motion) this.app.syncRuntimeTicker();
164
+ this.crew.render({ active: true, motion: this.motion, elapsed: this.elapsed,
165
+ clearing: this.session.clearing, clearProgress: Math.min(1, this.session.clearElapsed / 650) });
166
+ return true;
167
+ }
168
+
169
+ destroy() {
170
+ this.navigation.destroy();
171
+ this.viewControls.destroy();
172
+ this.abort.abort();
173
+ try { this.crew.destroy(); } finally {
174
+ this.layout.destroy();
175
+ this.element.remove();
176
+ delete this.panel.dataset.gridWalkActive;
177
+ for (const layer of [this.app.trailLayer, this.app.sensorLayer, this.app.pitLaneStatusLayer]) if (layer) layer.visible = true;
178
+ }
179
+ }
180
+ }