@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
@@ -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
+ }