@inventure71/paddockjs 4.2.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 (249) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/INSTALL_AND_UPDATE.md +23 -4
  3. package/README.md +31 -2
  4. package/docs/advanced_ai_pace.md +131 -0
  5. package/docs/advanced_physics_rebuild.md +166 -0
  6. package/docs/architecture.md +109 -30
  7. package/docs/cleanup_coverage.md +284 -0
  8. package/docs/component_inventory.md +31 -15
  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/grid_walk.md +80 -0
  16. package/docs/index.md +1 -0
  17. package/docs/keyboard_driving_fix.md +118 -0
  18. package/docs/limit_driving_2026-10-14.md +88 -0
  19. package/docs/maintainability_audit_2026-09-08.md +146 -0
  20. package/docs/racing_ai_improvements.md +145 -0
  21. package/docs/rules.md +43 -13
  22. package/docs/sense_contract.md +4 -1
  23. package/docs/system_specs.md +36 -11
  24. package/docs/testing-reliability-guide.md +5 -0
  25. package/docs/v11_improvements.md +108 -0
  26. package/package.json +23 -13
  27. package/src/api/MountedSimulatorComponents.js +101 -0
  28. package/src/api/PaddockSimulatorController.js +56 -81
  29. package/src/app/BrowserExpertAdapter.js +0 -6
  30. package/src/app/BrowserExpertExternalRenderer.js +72 -50
  31. package/src/app/ExternalTrackSnapshot.js +50 -0
  32. package/src/app/F1SimulatorApp.js +242 -297
  33. package/src/app/HostLifecycleEmitter.js +93 -0
  34. package/src/app/banners/raceDataBannerController.js +9 -4
  35. package/src/app/camera/cameraConstants.js +1 -1
  36. package/src/app/camera/cameraController.js +10 -13
  37. package/src/app/camera/cameraGeometry.js +1 -1
  38. package/src/app/controls/SimulatorViewControls.js +45 -0
  39. package/src/app/controls/workbenchBindings.js +64 -0
  40. package/src/app/controls/workbenchDisclosure.js +41 -0
  41. package/src/app/domBindings.js +21 -13
  42. package/src/app/gridWalk/GridWalkController.js +180 -0
  43. package/src/app/gridWalk/gridWalkCamera.js +88 -0
  44. package/src/app/gridWalk/gridWalkControls.js +24 -0
  45. package/src/app/gridWalk/gridWalkCrew.js +246 -0
  46. package/src/app/gridWalk/gridWalkLabels.js +104 -0
  47. package/src/app/gridWalk/gridWalkLayout.js +46 -0
  48. package/src/app/gridWalk/gridWalkNavigation.js +99 -0
  49. package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
  50. package/src/app/gridWalk/gridWalkSession.js +50 -0
  51. package/src/app/layoutSupport.js +2 -0
  52. package/src/app/raceOverlayClearanceSupport.js +21 -7
  53. package/src/app/readouts/carOverviewRenderer.js +9 -4
  54. package/src/app/readouts/readoutFormatters.js +1 -8
  55. package/src/app/readouts/telemetryRenderer.js +1 -3
  56. package/src/app/readouts/timingTowerRenderer.js +48 -50
  57. package/src/app/rendering/appAssets.js +8 -10
  58. package/src/app/rendering/carRenderer.js +27 -79
  59. package/src/app/rendering/displayUtils.js +0 -6
  60. package/src/app/rendering/expertSensorRenderer.js +1 -1
  61. package/src/app/rendering/replayGhostRenderer.js +4 -1
  62. package/src/app/rendering/trackRenderer.js +2 -2
  63. package/src/app/runtime/frameLoop.js +18 -17
  64. package/src/app/runtime/pixiStartupLifecycle.js +94 -0
  65. package/src/app/runtime/runtimeTiming.js +4 -1
  66. package/src/app/runtime/runtimeVisibility.js +7 -2
  67. package/src/config/cssValues.js +1 -1
  68. package/src/config/defaultOptions.js +12 -173
  69. package/src/config/restartOptions.js +1 -0
  70. package/src/config/runtimeSeed.js +10 -0
  71. package/src/config/simulatorDefaults.js +106 -0
  72. package/src/config/telemetryModules.js +20 -0
  73. package/src/config/theme/cssTokens.js +85 -0
  74. package/src/config/theme/themeCss.js +72 -0
  75. package/src/config/theme/themeInputs.js +155 -0
  76. package/src/config/theme/themeResolver.js +172 -0
  77. package/src/config/theme/themeSelectors.js +69 -0
  78. package/src/config/theme/themeTokens.js +257 -0
  79. package/src/config/theme/themeUtils.js +15 -0
  80. package/src/config/themeOptions.js +14 -830
  81. package/src/config/timingGapMode.js +0 -2
  82. package/src/config/uiOptions.js +53 -0
  83. package/src/data/championship.js +16 -158
  84. package/src/data/championshipDefaults.js +87 -0
  85. package/src/data/driverData.js +10 -20
  86. package/src/data/entryIdentity.js +67 -0
  87. package/src/data/entryPresentation.js +23 -0
  88. package/src/data/entryTypes.d.ts +160 -0
  89. package/src/data/index.d.ts +13 -259
  90. package/src/data/ratingScale.js +14 -0
  91. package/src/data/teamTypes.d.ts +23 -0
  92. package/src/data/trackTypes.d.ts +101 -0
  93. package/src/data/vehicleData.js +12 -23
  94. package/src/environment/actions.js +3 -2
  95. package/src/environment/controllerLoop.js +57 -44
  96. package/src/environment/controllerScheduler.js +30 -0
  97. package/src/environment/driverObservations.js +260 -0
  98. package/src/environment/driverReset.js +51 -0
  99. package/src/environment/episode.js +0 -1
  100. package/src/environment/evaluation.js +6 -90
  101. package/src/environment/evaluationTracker.js +100 -0
  102. package/src/environment/events.js +23 -0
  103. package/src/environment/index.d.ts +21 -140
  104. package/src/environment/metrics.js +3 -17
  105. package/src/environment/observationSchema.js +104 -0
  106. package/src/environment/observationVector.js +16 -115
  107. package/src/environment/observations.js +16 -316
  108. package/src/environment/options.js +5 -24
  109. package/src/environment/renderFrames.js +21 -0
  110. package/src/environment/resetOptions.js +41 -0
  111. package/src/environment/results.js +125 -0
  112. package/src/environment/rewards.js +10 -16
  113. package/src/environment/runtime.js +17 -300
  114. package/src/environment/scenarioOptions.js +90 -0
  115. package/src/environment/scenarioPlacements.js +70 -0
  116. package/src/environment/scenarioPresets.js +78 -0
  117. package/src/environment/scenarios.js +5 -236
  118. package/src/environment/scratch.js +18 -0
  119. package/src/environment/sensorOptions.js +20 -0
  120. package/src/environment/sensors/index.js +2 -556
  121. package/src/environment/sensors/indexedRayBands.js +0 -18
  122. package/src/environment/sensors/nearbyCars.js +18 -15
  123. package/src/environment/sensors/opponentRadar.js +3 -2
  124. package/src/environment/sensors/pitConnectorProximity.js +20 -0
  125. package/src/environment/sensors/rayBandTrace.js +1 -37
  126. package/src/environment/sensors/rayBatch.js +81 -0
  127. package/src/environment/sensors/rayConfig.js +26 -0
  128. package/src/environment/sensors/rayGeometry.js +0 -8
  129. package/src/environment/sensors/rayGuards.js +0 -6
  130. package/src/environment/sensors/rayOutputs.js +75 -0
  131. package/src/environment/sensors/rayQuery.js +110 -0
  132. package/src/environment/sensors/rayScratch.js +146 -0
  133. package/src/environment/sensors/sensorTargets.js +12 -4
  134. package/src/environment/sensors/surfaceRays.js +6 -61
  135. package/src/environment/sensors/trackRays.js +7 -80
  136. package/src/environment/simulationSetup.js +30 -0
  137. package/src/environment/specs.js +15 -109
  138. package/src/index.d.ts +34 -259
  139. package/src/index.js +28 -6
  140. package/src/placeholder/index.js +1 -8
  141. package/src/rendering/displayLifecycle.js +6 -0
  142. package/src/rendering/proceduralTrackAsset.js +1 -1
  143. package/src/rendering/renderSnapshot.js +3 -0
  144. package/src/rendering/track/finishLineRenderer.js +1 -1
  145. package/src/rendering/track/gridRenderer.js +1 -1
  146. package/src/rendering/track/offsetStrokeSafety.js +2 -2
  147. package/src/rendering/track/pitLaneRenderer.js +1 -1
  148. package/src/rendering/track/trackMaterialRenderer.js +4 -4
  149. package/src/rendering/track/trackRenderConstants.js +0 -1
  150. package/src/rendering/track/trackRenderGeometry.js +1 -7
  151. package/src/simulation/collisionGeometry.js +5 -11
  152. package/src/simulation/driver/advancedPathControls.js +53 -0
  153. package/src/simulation/driver/advancedRacePace.js +112 -0
  154. package/src/simulation/driver/advancedRacingControls.js +61 -0
  155. package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
  156. package/src/simulation/driver/advancedRacingSteering.js +76 -0
  157. package/src/simulation/driver/advancedRacingTiming.js +16 -0
  158. package/src/simulation/driver/arcadeRacePace.js +83 -0
  159. package/src/simulation/driver/arcadeRacingControls.js +111 -0
  160. package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
  161. package/src/simulation/driver/driverControlConstants.js +0 -2
  162. package/src/simulation/driver/driverController.js +14 -3
  163. package/src/simulation/driver/edgeRecovery.js +0 -1
  164. package/src/simulation/driver/racingControls.js +10 -406
  165. package/src/simulation/driver/racingDriverProfile.js +48 -0
  166. package/src/simulation/driver/racingLinePlan.js +48 -15
  167. package/src/simulation/driver/rejoinControls.js +58 -111
  168. package/src/simulation/driver/safetyCarControls.js +12 -0
  169. package/src/simulation/driver/trafficScan.js +89 -6
  170. package/src/simulation/pit/pitCarPlacement.js +29 -0
  171. package/src/simulation/pit/pitFlow.js +8 -30
  172. package/src/simulation/pit/pitIntent.js +4 -3
  173. package/src/simulation/pit/pitOccupancy.js +2 -2
  174. package/src/simulation/pit/pitQueue.js +2 -18
  175. package/src/simulation/pit/pitRouteMovement.js +1 -36
  176. package/src/simulation/pit/pitRouting.js +5 -81
  177. package/src/simulation/pit/pitService.js +3 -32
  178. package/src/simulation/pit/pitServiceConstants.js +1 -0
  179. package/src/simulation/pit/pitServiceProgress.js +34 -0
  180. package/src/simulation/pit/pitSnapshots.js +8 -4
  181. package/src/simulation/pit/pitState.js +1 -5
  182. package/src/simulation/race/classification.js +1 -8
  183. package/src/simulation/race/raceFinish.js +3 -8
  184. package/src/simulation/race/raceOrder.js +2 -13
  185. package/src/simulation/race/raceProgress.js +6 -20
  186. package/src/simulation/race/raceSetup.js +1 -1
  187. package/src/simulation/race/raceStep.js +1 -1
  188. package/src/simulation/race/redFlag.js +2 -4
  189. package/src/simulation/race/safetyCar.js +3 -8
  190. package/src/simulation/race/stalledDnf.js +2 -4
  191. package/src/simulation/raceSimulation.js +6 -38
  192. package/src/simulation/replay/replayGhosts.js +44 -32
  193. package/src/simulation/rules/collisionSteward.js +0 -14
  194. package/src/simulation/rules/penaltyActions.js +37 -0
  195. package/src/simulation/rules/penaltyLedger.js +8 -1
  196. package/src/simulation/rules/rulesReview.js +1 -7
  197. package/src/simulation/simMath.js +4 -0
  198. package/src/simulation/snapshots/raceSnapshots.js +30 -11
  199. package/src/simulation/timing/drsState.js +8 -0
  200. package/src/simulation/timing/drsTiming.js +2 -4
  201. package/src/simulation/timing/lapTelemetry.js +7 -4
  202. package/src/simulation/timing/sectorPerformance.js +3 -3
  203. package/src/simulation/timing/timingLines.js +12 -6
  204. package/src/simulation/track/pitLaneAccess.js +2 -2
  205. package/src/simulation/track/pitLaneLayout.js +4 -135
  206. package/src/simulation/track/pitLaneModel.js +70 -0
  207. package/src/simulation/track/pitLaneState.js +0 -1
  208. package/src/simulation/track/pitServiceLayout.js +121 -0
  209. package/src/simulation/track/proceduralTrack.js +77 -0
  210. package/src/simulation/track/spatialQueries.js +1 -25
  211. package/src/simulation/track/trackBuilder.js +54 -0
  212. package/src/simulation/track/trackConstants.js +0 -22
  213. package/src/simulation/track/trackMath.js +4 -29
  214. package/src/simulation/track/trackModel.js +4 -241
  215. package/src/simulation/track/trackModelCache.js +105 -0
  216. package/src/simulation/track/trackNearestQueries.js +1218 -0
  217. package/src/simulation/track/trackQueryConstants.js +3 -0
  218. package/src/simulation/track/trackQueryConstruction.js +368 -0
  219. package/src/simulation/track/trackQueryIndex.js +12 -1933
  220. package/src/simulation/track/trackQuerySegments.js +174 -0
  221. package/src/simulation/track/trackQueryStats.js +12 -0
  222. package/src/simulation/track/trackRayQueries.js +165 -0
  223. package/src/simulation/vehicle/advancedContactResponse.js +89 -0
  224. package/src/simulation/vehicle/advancedTireForces.js +92 -0
  225. package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
  226. package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
  227. package/src/simulation/vehicle/contactResolution.js +20 -6
  228. package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
  229. package/src/simulation/vehicle/pitWheelSurface.js +0 -12
  230. package/src/simulation/vehicle/runoffResponse.js +2 -4
  231. package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
  232. package/src/simulation/vehicle/vehicleGeometry.js +0 -56
  233. package/src/simulation/vehicle/vehicleKinematics.js +10 -0
  234. package/src/simulation/vehicle/vehiclePhysics.js +35 -194
  235. package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
  236. package/src/simulation/vehicle/vehicleState.js +2 -1
  237. package/src/simulation/vehicle/wheelSurface.js +13 -432
  238. package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
  239. package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
  240. package/src/styles.css +173 -19
  241. package/src/ui/bannerTemplates.js +4 -2
  242. package/src/ui/cameraControlsTemplate.js +4 -2
  243. package/src/ui/gridWalkTemplate.js +29 -0
  244. package/src/ui/htmlEscaping.js +8 -0
  245. package/src/ui/shellTemplate.js +3 -0
  246. package/src/ui/telemetryDrawerTemplate.js +4 -2
  247. package/src/ui/telemetryTemplates.js +3 -22
  248. package/src/ui/templateUtils.js +2 -8
  249. package/src/ui/viewControlsTemplate.js +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "type": "module",
5
5
  "description": "A data-driven browser F1 simulator component for host websites.",
6
6
  "license": "Apache-2.0",
@@ -88,8 +88,10 @@
88
88
  "test:fast": "node scripts/run-vitest.mjs",
89
89
  "types:check": "tsc -p tsconfig.types.json --noEmit",
90
90
  "docs:check": "node scripts/docs-check.mjs",
91
- "check": "npm run docs:check && npm run test:fast && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run consumer:bundle-boundaries && npm run showcase:ci && npm run browser:smoke:quick -- --skip-build",
92
- "check:release": "npm run docs:check && npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run consumer:bundle-boundaries && npm run showcase:ci && npm run browser:smoke:full -- --skip-build",
91
+ "check": "npm run docs:check && npm run test:fast && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run consumer:bundle-boundaries && npm run site:ci && npm run browser:smoke:quick -- --skip-build && npm run demo:smoke -- --skip-build",
92
+ "check:release": "npm run docs:check && npm run audit:release && npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run consumer:bundle-boundaries && npm run site:ci && npm run browser:smoke:full -- --skip-build && npm run demo:smoke -- --skip-build",
93
+ "audit:release": "npm run tools:release:ci && npm audit --audit-level=high && npm --prefix site audit --audit-level=high && npm --prefix tools/release audit --audit-level=high",
94
+ "tools:release:ci": "npm --prefix tools/release ci",
93
95
  "pack:dry": "npm pack --dry-run",
94
96
  "consumer:smoke": "node scripts/consumer-package-smoke.mjs",
95
97
  "consumer:bundle-boundaries": "node scripts/consumer-bundle-boundaries.mjs",
@@ -97,21 +99,29 @@
97
99
  "browser:smoke:quick": "node smoke/browser-smoke.mjs --quick",
98
100
  "browser:smoke:full": "node smoke/browser-smoke.mjs --full",
99
101
  "benchmark:runtime": "node scripts/benchmark-runtime-efficiency.mjs",
100
- "showcase:install": "npm --prefix local-preview ci",
101
- "showcase:dev": "npm --prefix local-preview run dev",
102
- "showcase:build": "npm --prefix local-preview run build",
103
- "showcase:ci": "npm --prefix local-preview ci && npm --prefix local-preview run build",
104
- "changeset": "changeset",
105
- "version-packages": "changeset version",
106
- "release": "npm publish --provenance"
102
+ "showcase:install": "npm run site:install",
103
+ "showcase:dev": "npm run site:dev",
104
+ "showcase:build": "npm run site:build",
105
+ "showcase:ci": "npm run site:ci",
106
+ "demo:install": "npm run site:install",
107
+ "demo:dev": "npm run site:dev",
108
+ "demo:build": "npm run site:build",
109
+ "demo:ci": "npm run site:ci",
110
+ "demo:smoke": "node smoke/demo-smoke.mjs",
111
+ "changeset": "node scripts/run-changeset.mjs",
112
+ "version-packages": "node scripts/run-changeset.mjs version",
113
+ "release": "npm publish --provenance",
114
+ "site:install": "npm --prefix site ci",
115
+ "site:dev": "npm --prefix site run dev",
116
+ "site:build": "npm --prefix site run check",
117
+ "site:ci": "npm --prefix site ci && npm --prefix site run check"
107
118
  },
108
119
  "dependencies": {
109
- "pixi.js": "8.18.1"
120
+ "pixi.js": "8.20.1"
110
121
  },
111
122
  "devDependencies": {
112
- "@changesets/cli": "2.29.7",
113
123
  "playwright": "^1.57.0",
114
124
  "typescript": "5.9.3",
115
- "vitest": "4.1.5"
125
+ "vitest": "4.1.12"
116
126
  }
117
127
  }
@@ -0,0 +1,101 @@
1
+ import { installLayoutSupport } from '../app/layoutSupport.js';
2
+ import { applyPaddockThemeCssVariables } from '../config/defaultOptions.js';
3
+ import { formatCssUrl } from '../config/cssValues.js';
4
+
5
+ function assertMountTarget(root, label) {
6
+ if (!root || typeof root !== 'object' || !('innerHTML' in root)) {
7
+ throw new Error(`${label} requires a DOM element root.`);
8
+ }
9
+ }
10
+
11
+ function setPackageCssVariables(root, assets, theme, context = {}) {
12
+ root.classList?.add?.('f1-sim-component');
13
+ root.style?.setProperty?.('--broadcast-panel-surface', formatCssUrl(assets.broadcastPanel));
14
+ applyPaddockThemeCssVariables(root, theme, context);
15
+ }
16
+
17
+ function createCompositeRoot(getRoots, getOptions) {
18
+ return {
19
+ style: {
20
+ setProperty(name, value) {
21
+ getRoots().forEach((root) => root.style?.setProperty?.(name, value));
22
+ },
23
+ removeProperty(name) {
24
+ getRoots().forEach((root) => root.style?.removeProperty?.(name));
25
+ },
26
+ },
27
+ querySelector(selector) {
28
+ for (const root of getRoots()) {
29
+ const match = root.querySelector?.(selector);
30
+ if (match) return match;
31
+ }
32
+ return null;
33
+ },
34
+ querySelectorAll(selector) {
35
+ return getRoots().flatMap((root) => [...(root.querySelectorAll?.(selector) ?? [])]);
36
+ },
37
+ setAttribute(name, value) {
38
+ getRoots().forEach((root) => root.setAttribute?.(name, value));
39
+ },
40
+ removeAttribute(name) {
41
+ getRoots().forEach((root) => root.removeAttribute?.(name));
42
+ },
43
+ applyCssVariables(context = {}) {
44
+ const options = getOptions();
45
+ getRoots().forEach((root) => setPackageCssVariables(root, options.assets, options.theme, context));
46
+ },
47
+ };
48
+ }
49
+
50
+ // Owns mounted DOM surfaces, layout-support lifetimes and their composite query
51
+ // root. The simulator controller supplies runtime availability and options.
52
+ export class MountedSimulatorComponents {
53
+ constructor({ getOptions, canMount }) {
54
+ this.getOptions = getOptions;
55
+ this.canMount = canMount;
56
+ this.roots = new Map();
57
+ this.mountRenderers = new Map();
58
+ this.layoutSupportCleanups = new Map();
59
+ this.compositeRoot = createCompositeRoot(() => [...this.roots.values()], getOptions);
60
+ }
61
+
62
+ mount(root, key, createMarkup) {
63
+ assertMountTarget(root, `mount ${key}`);
64
+ if (!this.canMount()) {
65
+ throw new Error('Mount PaddockJS components before calling start().');
66
+ }
67
+ const occupiedEntry = [...this.roots.entries()]
68
+ .find(([mountedKey, mountedRoot]) => mountedKey !== key && mountedRoot === root);
69
+ if (occupiedEntry) {
70
+ throw new Error(`Mount ${key} requires its own root; this root already hosts ${occupiedEntry[0]}.`);
71
+ }
72
+ const render = typeof createMarkup === 'function' ? createMarkup : () => createMarkup;
73
+ const previousRoot = this.roots.get(key);
74
+ this.layoutSupportCleanups.get(key)?.();
75
+ if (previousRoot && previousRoot !== root) previousRoot.innerHTML = '';
76
+ root.innerHTML = render();
77
+ setPackageCssVariables(root, this.getOptions().assets, this.getOptions().theme);
78
+ this.layoutSupportCleanups.set(key, installLayoutSupport(root));
79
+ this.roots.set(key, root);
80
+ this.mountRenderers.set(key, render);
81
+ return root;
82
+ }
83
+
84
+ rerender() {
85
+ this.roots.forEach((root, key) => {
86
+ const render = this.mountRenderers.get(key);
87
+ if (render) root.innerHTML = render();
88
+ setPackageCssVariables(root, this.getOptions().assets, this.getOptions().theme);
89
+ this.layoutSupportCleanups.get(key)?.();
90
+ this.layoutSupportCleanups.set(key, installLayoutSupport(root));
91
+ });
92
+ }
93
+
94
+ destroy() {
95
+ this.layoutSupportCleanups.forEach((cleanup) => cleanup());
96
+ this.layoutSupportCleanups.clear();
97
+ this.roots.forEach((root) => { root.innerHTML = ''; });
98
+ this.roots.clear();
99
+ this.mountRenderers.clear();
100
+ }
101
+ }
@@ -1,5 +1,6 @@
1
+ import { createViewControlsMarkup } from '../ui/viewControlsTemplate.js';
1
2
  import { F1SimulatorApp } from '../app/F1SimulatorApp.js';
2
- import { installLayoutSupport } from '../app/layoutSupport.js';
3
+ import { MountedSimulatorComponents } from './MountedSimulatorComponents.js';
3
4
  import {
4
5
  createRaceDataPanelMarkup,
5
6
  createTelemetrySectorBannerMarkup,
@@ -18,80 +19,27 @@ import {
18
19
  createTelemetrySectorsMarkup,
19
20
  createTimingTowerMarkup,
20
21
  } from '../ui/componentTemplates.js';
21
- import { applyPaddockThemeCssVariables, resolveF1SimulatorOptions } from '../config/defaultOptions.js';
22
- import { formatCssUrl } from '../config/cssValues.js';
22
+ import { resolveF1SimulatorOptions } from '../config/defaultOptions.js';
23
23
  import { mergeRestartOptions } from '../config/restartOptions.js';
24
24
  import { getNextTimingGapMode, normalizeTimingGapMode } from '../config/timingGapMode.js';
25
25
  import { createThemeSync, resolveRuntimeThemeModeOptions, resolveRuntimeThemeOptions } from './runtimeTheme.js';
26
26
 
27
- function assertMountTarget(root, label) {
28
- if (!root || typeof root !== 'object' || !('innerHTML' in root)) {
29
- throw new Error(`${label} requires a DOM element root.`);
30
- }
31
- }
32
-
33
- function setPackageCssVariables(root, assets, theme, context = {}) {
34
- root.classList?.add?.('f1-sim-component');
35
- root.style?.setProperty?.('--broadcast-panel-surface', formatCssUrl(assets.broadcastPanel));
36
- applyPaddockThemeCssVariables(root, theme, context);
37
- }
38
-
39
- function createCompositeRoot(getRoots, getOptions) {
40
- return {
41
- style: {
42
- setProperty(name, value) {
43
- getRoots().forEach((root) => root.style?.setProperty?.(name, value));
44
- },
45
- removeProperty(name) {
46
- getRoots().forEach((root) => root.style?.removeProperty?.(name));
47
- },
48
- },
49
- querySelector(selector) {
50
- for (const root of getRoots()) {
51
- const match = root.querySelector?.(selector);
52
- if (match) return match;
53
- }
54
- return null;
55
- },
56
- querySelectorAll(selector) {
57
- return getRoots().flatMap((root) => [...(root.querySelectorAll?.(selector) ?? [])]);
58
- },
59
- setAttribute(name, value) {
60
- getRoots().forEach((root) => root.setAttribute?.(name, value));
61
- },
62
- removeAttribute(name) {
63
- getRoots().forEach((root) => root.removeAttribute?.(name));
64
- },
65
- applyCssVariables(context = {}) {
66
- const options = getOptions();
67
- getRoots().forEach((root) => setPackageCssVariables(root, options.assets, options.theme, context));
68
- },
69
- };
70
- }
71
-
72
27
  export class PaddockSimulatorController {
73
28
  constructor(options = {}) {
74
29
  this.options = resolveF1SimulatorOptions(options);
75
- this.roots = new Map();
76
- this.mountRenderers = new Map();
77
- this.layoutSupportCleanups = new Map();
78
30
  this.app = null;
79
- this.compositeRoot = createCompositeRoot(() => [...this.roots.values()], () => this.options);
31
+ this.initializingApp = null;
32
+ this.startPromise = null;
33
+ this.components = new MountedSimulatorComponents({
34
+ getOptions: () => this.options,
35
+ canMount: () => !this.app && !this.startPromise,
36
+ });
37
+ this.roots = this.components.roots;
38
+ this.compositeRoot = this.components.compositeRoot;
80
39
  }
81
40
 
82
41
  mountComponent(root, key, createMarkup) {
83
- assertMountTarget(root, `mount ${key}`);
84
- if (this.app) {
85
- throw new Error('Mount PaddockJS components before calling start().');
86
- }
87
- const render = typeof createMarkup === 'function' ? createMarkup : () => createMarkup;
88
- root.innerHTML = render();
89
- setPackageCssVariables(root, this.options.assets, this.options.theme);
90
- this.layoutSupportCleanups.get(key)?.();
91
- this.layoutSupportCleanups.set(key, installLayoutSupport(root));
92
- this.roots.set(key, root);
93
- this.mountRenderers.set(key, render);
94
- return root;
42
+ return this.components.mount(root, key, createMarkup);
95
43
  }
96
44
 
97
45
  mountRaceControls(root) {
@@ -102,6 +50,10 @@ export class PaddockSimulatorController {
102
50
  return this.mountComponent(root, 'camera-controls', () => createCameraControlsMarkup(this.options));
103
51
  }
104
52
 
53
+ mountViewControls(root) {
54
+ return this.mountComponent(root, 'view-controls', () => createViewControlsMarkup(this.options));
55
+ }
56
+
105
57
  mountSafetyCarControl(root) {
106
58
  return this.mountComponent(root, 'safety-car-control', () => createSafetyCarControlMarkup(this.options));
107
59
  }
@@ -176,10 +128,31 @@ export class PaddockSimulatorController {
176
128
 
177
129
  async start() {
178
130
  if (this.app) return this;
131
+ if (this.startPromise) return this.startPromise;
179
132
  this.compositeRoot.applyCssVariables();
180
- this.app = new F1SimulatorApp(this.compositeRoot, this.options);
181
- await this.app.init();
182
- return this;
133
+ const app = new F1SimulatorApp(this.compositeRoot, this.options);
134
+ this.initializingApp = app;
135
+ const startPromise = (async () => {
136
+ try {
137
+ await app.init();
138
+ if (this.initializingApp !== app) {
139
+ throw new Error('PaddockJS start() was cancelled before initialization completed.');
140
+ }
141
+ this.app = app;
142
+ this.initializingApp = null;
143
+ return this;
144
+ } catch (error) {
145
+ app.destroy();
146
+ throw error;
147
+ }
148
+ })();
149
+ this.startPromise = startPromise;
150
+ try {
151
+ return await startPromise;
152
+ } finally {
153
+ if (this.startPromise === startPromise) this.startPromise = null;
154
+ if (this.initializingApp === app) this.initializingApp = null;
155
+ }
183
156
  }
184
157
 
185
158
  get expert() {
@@ -187,25 +160,16 @@ export class PaddockSimulatorController {
187
160
  }
188
161
 
189
162
  destroy() {
163
+ const initializingApp = this.initializingApp;
164
+ this.initializingApp = null;
165
+ initializingApp?.destroy();
190
166
  this.app?.destroy();
191
167
  this.app = null;
192
- this.layoutSupportCleanups.forEach((cleanup) => cleanup());
193
- this.layoutSupportCleanups.clear();
194
- this.roots.forEach((root) => {
195
- root.innerHTML = '';
196
- });
197
- this.roots.clear();
198
- this.mountRenderers.clear();
168
+ this.components.destroy();
199
169
  }
200
170
 
201
171
  rerenderMountedComponents() {
202
- this.roots.forEach((root, key) => {
203
- const render = this.mountRenderers.get(key);
204
- if (render) root.innerHTML = render();
205
- setPackageCssVariables(root, this.options.assets, this.options.theme);
206
- this.layoutSupportCleanups.get(key)?.();
207
- this.layoutSupportCleanups.set(key, installLayoutSupport(root));
208
- });
172
+ this.components.rerender();
209
173
  }
210
174
 
211
175
  syncOptionsFromRunningApp() {
@@ -222,6 +186,9 @@ export class PaddockSimulatorController {
222
186
  }
223
187
 
224
188
  restart(nextOptions = {}) {
189
+ if (this.startPromise || this.initializingApp) {
190
+ throw new Error('Cannot restart PaddockJS while start() is initializing. Await start() before restarting.');
191
+ }
225
192
  const currentOptions = this.app ? this.syncOptionsFromRunningApp() : this.options;
226
193
  const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(currentOptions, nextOptions));
227
194
  if (this.app) {
@@ -275,6 +242,10 @@ export class PaddockSimulatorController {
275
242
  this.app?.selectCar(driverId, { focus: true });
276
243
  }
277
244
 
245
+ setView(view) { return this.app?.setView(view) ?? false; }
246
+
247
+ getView() { return this.app?.getView() ?? (this.options.gridWalk.enabled ? this.options.gridWalk.initialView : 'race'); }
248
+
278
249
  setSafetyCarDeployed(deployed) {
279
250
  this.app?.setSafetyCarDeployed(deployed);
280
251
  }
@@ -374,6 +345,10 @@ export function mountCameraControls(root, simulator) {
374
345
  return simulator.mountCameraControls(root);
375
346
  }
376
347
 
348
+ export function mountViewControls(root, simulator) {
349
+ return simulator.mountViewControls(root);
350
+ }
351
+
377
352
  export function mountCarDriverOverview(root, simulator) {
378
353
  return simulator.mountCarDriverOverview(root);
379
354
  }
@@ -40,12 +40,6 @@ export function createBrowserExpertAdapter(app, expertOptions = {}) {
40
40
  });
41
41
  }
42
42
 
43
- function initialResolvedOptions() {
44
- return resolveBrowserExpertOptions(app.options, expertOptions);
45
- }
46
-
47
- resolvedOptions = initialResolvedOptions();
48
-
49
43
  const runtime = createEnvironmentRuntime({
50
44
  getSimulation: () => app.sim,
51
45
  setSimulation(nextSim) {
@@ -1,3 +1,4 @@
1
+ import { createExternalTrackSnapshotDecoder } from './ExternalTrackSnapshot.js';
1
2
  import { renderTrackSurface } from './rendering/trackRenderer.js';
2
3
 
3
4
  export function createBrowserExpertExternalRendererBridge(app) {
@@ -7,8 +8,11 @@ export function createBrowserExpertExternalRendererBridge(app) {
7
8
  let lastFrameAt = null;
8
9
  let lastError = null;
9
10
  let driverIdMap = new Map();
10
- let trackFingerprint = null;
11
+ let renderedTrack = null;
12
+ let renderedFingerprint = null;
11
13
  let appliedTrackSurface = false;
14
+ let attachmentGeneration = 0;
15
+ const trackDecoder = createExternalTrackSnapshotDecoder();
12
16
 
13
17
  function localDriverRecords() {
14
18
  const source = Array.isArray(app.drivers) && app.drivers.length > 0
@@ -37,14 +41,16 @@ export function createBrowserExpertExternalRendererBridge(app) {
37
41
  const knownLocalIdSet = new Set(knownLocalIds);
38
42
 
39
43
  const activeRawIds = new Set();
40
- const usedLocalIds = new Set();
44
+ // Reserve exact identities before allocating unknown external IDs. A
45
+ // leading unknown car must not steal a later car's existing local slot.
46
+ const usedLocalIds = new Set(snapshot.cars.map((car) => car?.id).filter((id) => knownLocalIdSet.has(id)));
41
47
 
42
48
  snapshot.cars.forEach((car, index) => {
43
49
  const rawId = typeof car?.id === 'string' && car.id.length > 0
44
50
  ? car.id
45
51
  : `external-${String(index)}`;
46
52
  activeRawIds.add(rawId);
47
- if (knownLocalIdSet.has(rawId) && !usedLocalIds.has(rawId)) {
53
+ if (knownLocalIdSet.has(rawId)) {
48
54
  driverIdMap.set(rawId, rawId);
49
55
  usedLocalIds.add(rawId);
50
56
  return;
@@ -95,6 +101,7 @@ export function createBrowserExpertExternalRendererBridge(app) {
95
101
  const mappedEvents = Array.isArray(snapshot.events)
96
102
  ? snapshot.events.map((event) => ({
97
103
  ...event,
104
+ driverId: mapId(event?.driverId),
98
105
  carId: mapId(event?.carId),
99
106
  otherCarId: mapId(event?.otherCarId),
100
107
  winnerId: mapId(event?.winnerId),
@@ -194,18 +201,23 @@ export function createBrowserExpertExternalRendererBridge(app) {
194
201
 
195
202
  function syncTrackSurface(snapshot) {
196
203
  if (!snapshot || typeof snapshot !== 'object') return;
197
- const nextFingerprint = fingerprintTrack(snapshot);
198
- if (!nextFingerprint) return;
199
- if (nextFingerprint === trackFingerprint) return;
204
+ const track = snapshot.track;
205
+ if (!track || typeof track !== 'object') return;
206
+ const fingerprint = fingerprintTrack(snapshot);
207
+ if (track === renderedTrack && fingerprint === renderedFingerprint) return;
200
208
  if (
201
209
  !app.trackAsset ||
202
210
  !app.drsLayer ||
203
211
  !app.sensorLayer ||
204
212
  !app.pitLaneStatusLayer
205
213
  ) {
206
- trackFingerprint = nextFingerprint;
214
+ renderedTrack = track;
215
+ renderedFingerprint = fingerprint;
207
216
  return;
208
217
  }
218
+ // Drawing clears layers before it can fail. Own restoration from this
219
+ // point, including partial drawing failures on the first external packet.
220
+ appliedTrackSurface = true;
209
221
  app.pitLaneStatusRenderer?.reset?.();
210
222
  app.cameraController?.invalidateTrackCaches?.();
211
223
  renderTrackSurface({
@@ -215,8 +227,8 @@ export function createBrowserExpertExternalRendererBridge(app) {
215
227
  trackAsset: app.trackAsset,
216
228
  snapshot,
217
229
  });
218
- trackFingerprint = nextFingerprint;
219
- appliedTrackSurface = true;
230
+ renderedTrack = track;
231
+ renderedFingerprint = fingerprint;
220
232
  }
221
233
 
222
234
  function assertDetached(method) {
@@ -225,25 +237,26 @@ export function createBrowserExpertExternalRendererBridge(app) {
225
237
  }
226
238
 
227
239
  function detach() {
228
- if (typeof unsubscribeExternalRenderer === 'function') {
229
- try {
230
- unsubscribeExternalRenderer();
231
- } catch (error) {
232
- lastError = error instanceof Error ? error.message : String(error);
233
- }
234
- }
240
+ // Clear ownership before cleanup: unsubscribe may flush a queued packet or
241
+ // reentrantly install its successor. Neither can revive this attachment.
242
+ const generation = ++attachmentGeneration;
243
+ const unsubscribe = unsubscribeExternalRenderer;
244
+ const restoreTrack = appliedTrackSurface;
235
245
  unsubscribeExternalRenderer = null;
236
246
  attached = false;
237
247
  driverIdMap = new Map();
238
- if (appliedTrackSurface && typeof app.renderTrack === 'function') {
239
- try {
240
- app.renderTrack();
241
- } catch (error) {
242
- lastError = error instanceof Error ? error.message : String(error);
243
- }
244
- }
245
- trackFingerprint = null;
248
+ renderedTrack = null;
249
+ renderedFingerprint = null;
246
250
  appliedTrackSurface = false;
251
+ trackDecoder.reset();
252
+ if (restoreTrack && typeof app.renderTrack === 'function') {
253
+ try { app.renderTrack(); }
254
+ catch (error) { if (generation === attachmentGeneration) lastError = error instanceof Error ? error.message : String(error); }
255
+ }
256
+ if (typeof unsubscribe === 'function') {
257
+ try { unsubscribe(); }
258
+ catch (error) { if (generation === attachmentGeneration) lastError = error instanceof Error ? error.message : String(error); }
259
+ }
247
260
  }
248
261
 
249
262
  function attach(source) {
@@ -252,33 +265,42 @@ export function createBrowserExpertExternalRendererBridge(app) {
252
265
  }
253
266
  detach();
254
267
  lastError = null;
255
- const unsubscribe = source.subscribe((frame) => {
256
- if (!frame || typeof frame !== 'object') {
257
- lastError = 'Invalid external frame payload.';
258
- return;
259
- }
260
- const rewrittenFrame = rewriteFrame(frame);
261
- const snapshot = rewrittenFrame.snapshot;
262
- if (!snapshot || typeof snapshot !== 'object') {
263
- lastError = 'External frame is missing snapshot.';
264
- return;
265
- }
266
- try {
267
- syncTrackSurface(snapshot);
268
- app.renderExpertFrame(snapshot, {
269
- forceDomUpdate: true,
270
- observation: rewrittenFrame.observation && typeof rewrittenFrame.observation === 'object'
271
- ? rewrittenFrame.observation
272
- : {},
273
- });
274
- lastMeta = rewrittenFrame.meta ?? null;
275
- lastFrameAt = Date.now();
276
- lastError = null;
277
- } catch (error) {
278
- lastError = error instanceof Error ? error.message : String(error);
279
- }
280
- });
268
+ const generation = attachmentGeneration;
269
+ const isCurrent = () => generation === attachmentGeneration;
270
+ let unsubscribe;
271
+ try {
272
+ unsubscribe = source.subscribe((frame) => {
273
+ if (!isCurrent()) return;
274
+ if (!frame || typeof frame !== 'object') {
275
+ lastError = 'Invalid external frame payload.';
276
+ return;
277
+ }
278
+ try {
279
+ const rewrittenFrame = rewriteFrame(frame);
280
+ const snapshot = trackDecoder.decode(rewrittenFrame.snapshot);
281
+ if (!snapshot || typeof snapshot !== 'object') {
282
+ lastError = 'External frame is missing snapshot.';
283
+ return;
284
+ }
285
+ syncTrackSurface(snapshot);
286
+ app.renderExpertFrame(snapshot, {
287
+ forceDomUpdate: true,
288
+ observation: rewrittenFrame.observation && typeof rewrittenFrame.observation === 'object'
289
+ ? rewrittenFrame.observation
290
+ : {},
291
+ });
292
+ if (!isCurrent()) return;
293
+ lastMeta = rewrittenFrame.meta ?? null;
294
+ lastFrameAt = Date.now();
295
+ lastError = null;
296
+ } catch (error) {
297
+ if (isCurrent()) lastError = error instanceof Error ? error.message : String(error);
298
+ }
299
+ });
300
+ } catch (error) { if (isCurrent()) detach(); throw error; }
301
+ if (!isCurrent()) { if (typeof unsubscribe === 'function') unsubscribe(); return; }
281
302
  if (typeof unsubscribe !== 'function') {
303
+ detach();
282
304
  throw new Error('attachExternalRenderer(source) subscribe(onFrame) must return an unsubscribe function.');
283
305
  }
284
306
  unsubscribeExternalRenderer = unsubscribe;
@@ -0,0 +1,50 @@
1
+ import { attachTrackQueryIndex, createTrackQueryIndex } from '../simulation/track/trackQueryConstruction.js';
2
+ import { prepareSampleIndexLut } from '../simulation/track/spatialQueries.js';
3
+ import { createPitBoxBounds, createPitConnectorBounds, createPitLaneBounds } from '../simulation/track/pitLaneLayout.js';
4
+
5
+ const SAMPLE_FIELDS = ['x', 'y', 'distance', 'heading', 'normalX', 'normalY', 'curvature'];
6
+
7
+ // JSON snapshots deliberately omit runtime indexes and compact sample objects
8
+ // into a schema/table. Restore only display query infrastructure around that
9
+ // supplied geometry. Never generate another track or reconstruct model senses.
10
+ export function createExternalTrackSnapshotDecoder() {
11
+ let lastKey = null, lastTrack = null;
12
+ return {
13
+ decode(snapshot) {
14
+ const track = snapshot?.track;
15
+ if (!track || track.queryIndex || !Array.isArray(track.samples)) return snapshot;
16
+ const key = JSON.stringify(track);
17
+ if (key !== lastKey) {
18
+ const decoded = decodeTrack(track);
19
+ lastTrack = decoded;
20
+ lastKey = key;
21
+ }
22
+ return { ...snapshot, track: lastTrack };
23
+ },
24
+ reset() { lastKey = null; lastTrack = null; },
25
+ };
26
+ }
27
+
28
+ function decodeTrack(track) {
29
+ const schema = track.sampleSchema;
30
+ const samples = track.samples?.map((sample) => Array.isArray(sample) && Array.isArray(schema)
31
+ ? Object.fromEntries(schema.map((field, index) => [field, sample[index]]))
32
+ : { ...sample });
33
+ if (!Array.isArray(samples) || samples.length < 2 ||
34
+ !Number.isFinite(track.length) || track.length <= 0 ||
35
+ !Number.isFinite(track.width) || track.width <= 0 ||
36
+ samples.some((sample) => SAMPLE_FIELDS.some((field) => !Number.isFinite(sample[field])))) {
37
+ throw new Error('External track requires finite sample geometry, length and width.');
38
+ }
39
+ const decoded = { ...track, samples };
40
+ if (track.pitLane?.enabled) {
41
+ const pitLane = { ...track.pitLane };
42
+ pitLane.bounds ??= createPitLaneBounds(pitLane);
43
+ pitLane.boxBounds ??= createPitBoxBounds(pitLane);
44
+ pitLane.connectorBounds ??= createPitConnectorBounds(pitLane);
45
+ decoded.pitLane = pitLane;
46
+ }
47
+ attachTrackQueryIndex(decoded, createTrackQueryIndex(decoded));
48
+ prepareSampleIndexLut(decoded);
49
+ return decoded;
50
+ }