@inventure71/paddockjs 1.0.0 → 2.0.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 (190) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/INSTALL_AND_UPDATE.md +4 -3
  3. package/README.md +194 -12
  4. package/docs/architecture.md +119 -44
  5. package/docs/concepts.md +4 -2
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +299 -17
  8. package/docs/index.md +1 -0
  9. package/docs/learnings.md +22 -0
  10. package/docs/rules.md +37 -10
  11. package/docs/system_specs.md +23 -10
  12. package/docs/training.md +272 -30
  13. package/examples/python/README.md +62 -0
  14. package/examples/python/base_policy_server.py +191 -0
  15. package/examples/python/requirements.txt +2 -0
  16. package/package.json +12 -4
  17. package/src/api/PaddockSimulatorController.js +4 -0
  18. package/src/app/BrowserExpertAdapter.js +367 -9
  19. package/src/app/F1SimulatorApp.js +305 -1603
  20. package/src/app/banners/bannerFormatters.js +55 -0
  21. package/src/app/banners/raceDataBannerController.js +141 -0
  22. package/src/app/banners/stewardMessageController.js +99 -0
  23. package/src/app/camera/cameraConstants.js +21 -0
  24. package/src/app/camera/cameraController.js +265 -0
  25. package/src/app/camera/cameraGeometry.js +176 -0
  26. package/src/app/domBindings.js +4 -0
  27. package/src/app/readouts/carOverviewRenderer.js +109 -0
  28. package/src/app/readouts/controlStateRenderer.js +37 -0
  29. package/src/app/readouts/raceStatusRenderer.js +133 -0
  30. package/src/app/readouts/readoutFormatters.js +58 -0
  31. package/src/app/readouts/telemetryRenderer.js +113 -0
  32. package/src/app/readouts/timingTowerRenderer.js +177 -0
  33. package/src/app/rendering/appAssets.js +38 -0
  34. package/src/app/rendering/carRenderer.js +286 -0
  35. package/src/app/rendering/displayUtils.js +22 -0
  36. package/src/app/rendering/drsTrailRenderer.js +90 -0
  37. package/src/app/rendering/expertSensorRenderer.js +126 -0
  38. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  39. package/src/app/rendering/replayGhostRenderer.js +124 -0
  40. package/src/app/rendering/trackRenderer.js +25 -0
  41. package/src/app/runtime/frameLoop.js +66 -0
  42. package/src/app/runtime/runtimeTiming.js +34 -0
  43. package/src/app/runtime/runtimeVisibility.js +45 -0
  44. package/src/config/defaultOptions.js +20 -0
  45. package/src/environment/actions.js +13 -5
  46. package/src/environment/controllerLoop.js +246 -0
  47. package/src/environment/episode.js +86 -2
  48. package/src/environment/evaluation.js +54 -32
  49. package/src/environment/index.d.ts +361 -9
  50. package/src/environment/index.js +1 -0
  51. package/src/environment/metrics.js +159 -0
  52. package/src/environment/observationOptions.js +22 -1
  53. package/src/environment/observations.js +325 -40
  54. package/src/environment/options.js +37 -5
  55. package/src/environment/runtime.js +222 -21
  56. package/src/environment/scenarios.js +37 -8
  57. package/src/environment/sensors/bodySenses.js +23 -0
  58. package/src/environment/sensors/boundarySenses.js +17 -0
  59. package/src/environment/sensors/carRays.js +75 -0
  60. package/src/environment/sensors/contactSenses.js +32 -0
  61. package/src/environment/sensors/index.js +157 -0
  62. package/src/environment/sensors/indexedRayBands.js +103 -0
  63. package/src/environment/sensors/nearbyCars.js +52 -0
  64. package/src/environment/sensors/opponentRadar.js +65 -0
  65. package/src/environment/sensors/rayConfig.js +110 -0
  66. package/src/environment/sensors/rayDefaults.js +6 -0
  67. package/src/environment/sensors/rayGeometry.js +60 -0
  68. package/src/environment/sensors/rayGuards.js +19 -0
  69. package/src/environment/sensors/sensorTargets.js +60 -0
  70. package/src/environment/sensors/surfaceRays.js +296 -0
  71. package/src/environment/sensors/trackRays.js +261 -0
  72. package/src/environment/sensors.js +1 -367
  73. package/src/environment/specs.js +100 -5
  74. package/src/environment/workerProtocol.js +13 -1
  75. package/src/index.d.ts +169 -6
  76. package/src/index.js +5 -0
  77. package/src/rendering/proceduralTrackAsset.js +20 -823
  78. package/src/rendering/renderSnapshot.js +38 -12
  79. package/src/rendering/track/finishLineRenderer.js +34 -0
  80. package/src/rendering/track/gridRenderer.js +47 -0
  81. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  82. package/src/rendering/track/pitLaneRenderer.js +248 -0
  83. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  84. package/src/rendering/track/trackRenderConstants.js +38 -0
  85. package/src/rendering/track/trackRenderGeometry.js +94 -0
  86. package/src/simulation/collisionGeometry.js +1 -1
  87. package/src/simulation/driver/driverControlConstants.js +28 -0
  88. package/src/simulation/driver/driverController.js +47 -0
  89. package/src/simulation/driver/driverInput.js +28 -0
  90. package/src/simulation/driver/driverMath.js +6 -0
  91. package/src/simulation/driver/driverPersonality.js +25 -0
  92. package/src/simulation/driver/edgeRecovery.js +48 -0
  93. package/src/simulation/driver/racingControls.js +399 -0
  94. package/src/simulation/driver/racingLinePlan.js +166 -0
  95. package/src/simulation/driver/recoveryDynamics.js +38 -0
  96. package/src/simulation/driver/rejoinControls.js +213 -0
  97. package/src/simulation/driver/safetyCarControls.js +35 -0
  98. package/src/simulation/driver/trafficScan.js +51 -0
  99. package/src/simulation/driverController.js +4 -619
  100. package/src/simulation/participants/participantInteractions.js +120 -0
  101. package/src/simulation/pit/pitFlow.js +234 -0
  102. package/src/simulation/pit/pitIntent.js +92 -0
  103. package/src/simulation/pit/pitOccupancy.js +47 -0
  104. package/src/simulation/pit/pitPenaltyService.js +71 -0
  105. package/src/simulation/pit/pitQueue.js +55 -0
  106. package/src/simulation/pit/pitRouteMovement.js +160 -0
  107. package/src/simulation/pit/pitRouting.js +319 -0
  108. package/src/simulation/pit/pitService.js +41 -0
  109. package/src/simulation/pit/pitServiceConstants.js +21 -0
  110. package/src/simulation/pit/pitServiceProfile.js +45 -0
  111. package/src/simulation/pit/pitSnapshots.js +61 -0
  112. package/src/simulation/pit/pitState.js +113 -0
  113. package/src/simulation/pit/pitTireService.js +92 -0
  114. package/src/simulation/race/classification.js +104 -0
  115. package/src/simulation/race/gridPenalties.js +37 -0
  116. package/src/simulation/race/raceControlState.js +38 -0
  117. package/src/simulation/race/raceDistance.js +32 -0
  118. package/src/simulation/race/raceFinish.js +65 -0
  119. package/src/simulation/race/raceLifecycle.js +71 -0
  120. package/src/simulation/race/raceOrder.js +113 -0
  121. package/src/simulation/race/raceProgress.js +124 -0
  122. package/src/simulation/race/raceSetup.js +93 -0
  123. package/src/simulation/race/raceStep.js +80 -0
  124. package/src/simulation/race/redFlag.js +15 -0
  125. package/src/simulation/race/retirements.js +44 -0
  126. package/src/simulation/race/safetyCar.js +86 -0
  127. package/src/simulation/race/stalledDnf.js +86 -0
  128. package/src/simulation/raceSimulation.js +230 -2817
  129. package/src/simulation/replay/replayGhosts.js +128 -0
  130. package/src/simulation/rules/moduleConfig.js +72 -0
  131. package/src/simulation/rules/penaltyConfig.js +99 -0
  132. package/src/simulation/rules/penaltyEvents.js +18 -0
  133. package/src/simulation/rules/penaltyStats.js +26 -0
  134. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  135. package/src/simulation/rules/ruleDefaults.js +85 -0
  136. package/src/simulation/rules/rulesReview.js +141 -0
  137. package/src/simulation/rules/rulesetPresets.js +47 -0
  138. package/src/simulation/rules/trackLimitsSteward.js +1 -1
  139. package/src/simulation/rulesConfig.js +5 -323
  140. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  141. package/src/simulation/timing/drsTiming.js +87 -0
  142. package/src/simulation/timing/gapEstimation.js +38 -0
  143. package/src/simulation/timing/lapTelemetry.js +182 -0
  144. package/src/simulation/timing/raceTiming.js +15 -0
  145. package/src/simulation/timing/sectorPerformance.js +60 -0
  146. package/src/simulation/timing/timingConstants.js +5 -0
  147. package/src/simulation/timing/timingHistory.js +46 -0
  148. package/src/simulation/timing/timingLines.js +79 -0
  149. package/src/simulation/track/centerlineSpline.js +59 -0
  150. package/src/simulation/track/drsZones.js +67 -0
  151. package/src/simulation/track/pitLaneAccess.js +103 -0
  152. package/src/simulation/track/pitLaneLayout.js +395 -0
  153. package/src/simulation/track/pitLaneState.js +160 -0
  154. package/src/simulation/track/proceduralCenterline.js +237 -0
  155. package/src/simulation/track/sampleGeometry.js +49 -0
  156. package/src/simulation/track/spatialQueries.js +154 -0
  157. package/src/simulation/track/startStraight.js +141 -0
  158. package/src/simulation/track/trackConstants.js +109 -0
  159. package/src/simulation/track/trackGenerationOptions.js +283 -0
  160. package/src/simulation/track/trackMath.js +241 -0
  161. package/src/simulation/track/trackModel.js +240 -0
  162. package/src/simulation/track/trackQueryIndex.js +945 -0
  163. package/src/simulation/track/trackSectors.js +19 -0
  164. package/src/simulation/track/trackStatePolicy.js +24 -0
  165. package/src/simulation/track/trackValidation.js +98 -0
  166. package/src/simulation/trackModel.js +1 -1706
  167. package/src/simulation/vehicle/contactResolution.js +116 -0
  168. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  169. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  170. package/src/simulation/vehicle/runoffResponse.js +88 -0
  171. package/src/simulation/vehicle/surfacePriority.js +28 -0
  172. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  173. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  174. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  175. package/src/simulation/vehicle/vehicleState.js +186 -0
  176. package/src/simulation/vehicle/wheelSurface.js +150 -0
  177. package/src/simulation/vehicleGeometry.js +1 -247
  178. package/src/simulation/vehiclePhysics.js +1 -136
  179. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  180. package/src/simulation/wheelSurface.js +1 -338
  181. package/src/styles.css +42 -0
  182. package/src/ui/cameraControlsTemplate.js +20 -0
  183. package/src/ui/carOverviewTemplate.js +33 -0
  184. package/src/ui/componentTemplates.js +14 -360
  185. package/src/ui/raceCanvasTemplate.js +69 -0
  186. package/src/ui/raceControlsTemplate.js +36 -0
  187. package/src/ui/shellTemplate.js +5 -4
  188. package/src/ui/telemetryDrawerTemplate.js +55 -0
  189. package/src/ui/telemetryTemplates.js +136 -0
  190. package/src/ui/timingTowerTemplate.js +34 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "A data-driven browser F1 simulator component for host websites.",
6
6
  "license": "Apache-2.0",
@@ -53,7 +53,11 @@
53
53
  "LICENSE",
54
54
  "assets",
55
55
  "docs/*.md",
56
- "examples",
56
+ "examples/python/README.md",
57
+ "examples/python/base_policy_server.py",
58
+ "examples/python/requirements.txt",
59
+ "examples/train-basic-policy.mjs",
60
+ "examples/trainingData.mjs",
57
61
  "src/api",
58
62
  "src/app",
59
63
  "src/config",
@@ -68,12 +72,16 @@
68
72
  "README.md"
69
73
  ],
70
74
  "scripts": {
71
- "test": "vitest run src",
75
+ "test": "node scripts/run-vitest.mjs --slow",
76
+ "test:fast": "node scripts/run-vitest.mjs",
72
77
  "types:check": "tsc -p tsconfig.types.json --noEmit",
73
- "check": "npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run showcase:ci && npm run browser:smoke",
78
+ "check": "npm run test:fast && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run showcase:ci && npm run browser:smoke:quick -- --skip-build",
79
+ "check:release": "npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run showcase:ci && npm run browser:smoke:full -- --skip-build",
74
80
  "pack:dry": "npm pack --dry-run",
75
81
  "consumer:smoke": "node scripts/consumer-package-smoke.mjs",
76
82
  "browser:smoke": "node smoke/browser-smoke.mjs",
83
+ "browser:smoke:quick": "node smoke/browser-smoke.mjs --quick",
84
+ "browser:smoke:full": "node smoke/browser-smoke.mjs --full",
77
85
  "showcase:install": "npm --prefix local-preview ci",
78
86
  "showcase:dev": "npm --prefix local-preview run dev",
79
87
  "showcase:build": "npm --prefix local-preview run build",
@@ -250,6 +250,10 @@ export class PaddockSimulatorController {
250
250
  return this.app?.getPitTargetCompound?.(driverId) ?? null;
251
251
  }
252
252
 
253
+ getSimulationSpeed() {
254
+ return this.app?.simulationSpeed ?? 1;
255
+ }
256
+
253
257
  servePenalty(penaltyId) {
254
258
  return this.app?.servePenalty(penaltyId) ?? null;
255
259
  }
@@ -1,14 +1,268 @@
1
1
  import { createEnvironmentRuntime } from '../environment/runtime.js';
2
2
  import { resolveEnvironmentOptions } from '../environment/options.js';
3
+ import { renderTrackSurface } from './rendering/trackRenderer.js';
3
4
 
4
5
  export function createBrowserExpertAdapter(app, expertOptions = {}) {
5
- let resolvedOptions = resolveEnvironmentOptions({
6
- ...app.options,
7
- ...expertOptions,
8
- controlledDrivers: expertOptions.controlledDrivers,
9
- });
6
+ let resolvedOptions = resolveBrowserExpertOptions(app.options, expertOptions);
7
+ let frameRenderSuppressed = false;
8
+ let externalRendererUnsubscribe = null;
9
+ let externalRendererAttached = false;
10
+ let externalRendererLastMeta = null;
11
+ let externalRendererLastFrameAt = null;
12
+ let externalRendererLastError = null;
13
+ let externalRendererDriverIdMap = new Map();
14
+ let externalRendererTrackFingerprint = null;
15
+ let externalRendererAppliedTrackSurface = false;
16
+
17
+ function resolveBrowserExpertOptions(appOptions, nextExpertOptions = {}) {
18
+ const options = resolveEnvironmentOptions({
19
+ ...appOptions,
20
+ ...nextExpertOptions,
21
+ controlledDrivers: nextExpertOptions.controlledDrivers,
22
+ });
23
+ if (options.result.stateOutput !== 'none') return options;
24
+ return {
25
+ ...options,
26
+ result: {
27
+ ...options.result,
28
+ stateOutput: 'minimal',
29
+ },
30
+ };
31
+ }
32
+
33
+ function renderableSnapshot(result) {
34
+ return result?.state?.snapshot ?? app.sim?.snapshotObservation?.() ?? app.sim?.snapshot?.() ?? null;
35
+ }
36
+
37
+ /*
38
+ * Browser expert mode is a visual adapter. Compact headless options may ask for
39
+ * no state payload, but the canvas still needs a snapshot to render.
40
+ */
41
+ function renderExpertResult(result, renderOptions = {}) {
42
+ const snapshot = renderableSnapshot(result);
43
+ if (!snapshot) return;
44
+ app.renderExpertFrame(snapshot, {
45
+ ...renderOptions,
46
+ observation: result?.observation,
47
+ });
48
+ }
49
+
50
+ function initialResolvedOptions() {
51
+ return resolveBrowserExpertOptions(app.options, expertOptions);
52
+ }
53
+
54
+ resolvedOptions = initialResolvedOptions();
55
+
56
+ function localDriverRecords() {
57
+ const source = Array.isArray(app.drivers) && app.drivers.length > 0
58
+ ? app.drivers
59
+ : app.options?.drivers;
60
+ return Array.isArray(source) ? source : [];
61
+ }
62
+
63
+ function localDriverIds() {
64
+ return localDriverRecords()
65
+ .map((driver) => driver?.id)
66
+ .filter((id) => typeof id === 'string' && id.length > 0);
67
+ }
68
+
69
+ function localDriverById() {
70
+ return new Map(localDriverRecords().map((driver) => [driver.id, driver]));
71
+ }
72
+
73
+ function rewriteExternalFrame(frame) {
74
+ if (!frame || typeof frame !== 'object') return frame;
75
+ const snapshot = frame.snapshot;
76
+ if (!snapshot || typeof snapshot !== 'object' || !Array.isArray(snapshot.cars)) return frame;
77
+
78
+ const knownLocalIds = localDriverIds();
79
+ if (knownLocalIds.length === 0) return frame;
80
+ const knownLocalIdSet = new Set(knownLocalIds);
81
+
82
+ const activeRawIds = new Set();
83
+ const usedLocalIds = new Set();
84
+
85
+ snapshot.cars.forEach((car, index) => {
86
+ const rawId = typeof car?.id === 'string' && car.id.length > 0
87
+ ? car.id
88
+ : `external-${String(index)}`;
89
+ activeRawIds.add(rawId);
90
+ if (knownLocalIdSet.has(rawId) && !usedLocalIds.has(rawId)) {
91
+ externalRendererDriverIdMap.set(rawId, rawId);
92
+ usedLocalIds.add(rawId);
93
+ return;
94
+ }
95
+ const mapped = externalRendererDriverIdMap.get(rawId);
96
+ if (typeof mapped === 'string' && mapped.length > 0 && !usedLocalIds.has(mapped)) {
97
+ usedLocalIds.add(mapped);
98
+ return;
99
+ }
100
+ const nextLocalId = knownLocalIds.find((id) => !usedLocalIds.has(id));
101
+ if (nextLocalId) {
102
+ externalRendererDriverIdMap.set(rawId, nextLocalId);
103
+ usedLocalIds.add(nextLocalId);
104
+ return;
105
+ }
106
+ externalRendererDriverIdMap.set(rawId, rawId);
107
+ usedLocalIds.add(rawId);
108
+ });
109
+
110
+ for (const rawId of Array.from(externalRendererDriverIdMap.keys())) {
111
+ if (!activeRawIds.has(rawId)) externalRendererDriverIdMap.delete(rawId);
112
+ }
113
+
114
+ const mapId = (value) => {
115
+ if (typeof value !== 'string' || value.length === 0) return value;
116
+ return externalRendererDriverIdMap.get(value) ?? value;
117
+ };
118
+
119
+ const drivers = localDriverById();
120
+ const mappedCars = snapshot.cars.map((car, index) => {
121
+ const rawId = typeof car?.id === 'string' && car.id.length > 0
122
+ ? car.id
123
+ : `external-${String(index)}`;
124
+ const mappedId = mapId(rawId);
125
+ const driver = drivers.get(mappedId);
126
+ return {
127
+ ...car,
128
+ id: mappedId,
129
+ code: car?.code ?? driver?.code ?? mappedId,
130
+ timingCode: car?.timingCode ?? driver?.timingCode ?? driver?.code ?? mappedId,
131
+ name: car?.name ?? driver?.name ?? mappedId,
132
+ color: car?.color ?? driver?.color ?? '#9ca3af',
133
+ icon: car?.icon ?? driver?.icon ?? (driver?.code ?? mappedId),
134
+ team: car?.team ?? driver?.team ?? null,
135
+ };
136
+ });
137
+
138
+ const mappedEvents = Array.isArray(snapshot.events)
139
+ ? snapshot.events.map((event) => ({
140
+ ...event,
141
+ carId: mapId(event?.carId),
142
+ otherCarId: mapId(event?.otherCarId),
143
+ winnerId: mapId(event?.winnerId),
144
+ }))
145
+ : snapshot.events;
10
146
 
11
- return createEnvironmentRuntime({
147
+ const mappedPenalties = Array.isArray(snapshot.penalties)
148
+ ? snapshot.penalties.map((penalty) => ({
149
+ ...penalty,
150
+ driverId: mapId(penalty?.driverId),
151
+ }))
152
+ : snapshot.penalties;
153
+
154
+ const raceControl = snapshot.raceControl && typeof snapshot.raceControl === 'object'
155
+ ? {
156
+ ...snapshot.raceControl,
157
+ winner: snapshot.raceControl.winner && typeof snapshot.raceControl.winner === 'object'
158
+ ? {
159
+ ...snapshot.raceControl.winner,
160
+ id: mapId(snapshot.raceControl.winner.id),
161
+ }
162
+ : snapshot.raceControl.winner,
163
+ classification: Array.isArray(snapshot.raceControl.classification)
164
+ ? snapshot.raceControl.classification.map((entry) => ({
165
+ ...entry,
166
+ id: mapId(entry?.id),
167
+ driverId: mapId(entry?.driverId),
168
+ }))
169
+ : snapshot.raceControl.classification,
170
+ }
171
+ : snapshot.raceControl;
172
+
173
+ let mappedObservation = frame.observation;
174
+ if (frame.observation && typeof frame.observation === 'object' && !Array.isArray(frame.observation)) {
175
+ mappedObservation = {};
176
+ Object.entries(frame.observation).forEach(([driverId, value]) => {
177
+ mappedObservation[mapId(driverId)] = value;
178
+ });
179
+ }
180
+
181
+ return {
182
+ ...frame,
183
+ snapshot: {
184
+ ...snapshot,
185
+ cars: mappedCars,
186
+ events: mappedEvents,
187
+ penalties: mappedPenalties,
188
+ raceControl,
189
+ },
190
+ observation: mappedObservation,
191
+ meta: {
192
+ ...(frame.meta && typeof frame.meta === 'object' ? frame.meta : {}),
193
+ externalDriverMap: Object.fromEntries(externalRendererDriverIdMap.entries()),
194
+ },
195
+ };
196
+ }
197
+
198
+ function trackFingerprint(snapshot) {
199
+ const track = snapshot?.track;
200
+ if (!track || typeof track !== 'object') return null;
201
+ const drs = Array.isArray(track.drsZones)
202
+ ? track.drsZones.map((zone) => [zone?.id ?? null, zone?.start ?? null, zone?.end ?? null])
203
+ : [];
204
+ const firstSample = Array.isArray(track.samples) && track.samples.length > 0 ? track.samples[0] : null;
205
+ const lastSample = Array.isArray(track.samples) && track.samples.length > 0
206
+ ? track.samples[track.samples.length - 1]
207
+ : null;
208
+ return JSON.stringify({
209
+ name: track.name ?? null,
210
+ length: track.length ?? null,
211
+ width: track.width ?? null,
212
+ sampleCount: track.sampleCount ?? null,
213
+ finish: track.finish
214
+ ? {
215
+ x: track.finish.x ?? null,
216
+ y: track.finish.y ?? null,
217
+ heading: track.finish.heading ?? null,
218
+ }
219
+ : null,
220
+ firstSample: firstSample
221
+ ? {
222
+ x: firstSample.x ?? null,
223
+ y: firstSample.y ?? null,
224
+ heading: firstSample.heading ?? null,
225
+ }
226
+ : null,
227
+ lastSample: lastSample
228
+ ? {
229
+ x: lastSample.x ?? null,
230
+ y: lastSample.y ?? null,
231
+ heading: lastSample.heading ?? null,
232
+ }
233
+ : null,
234
+ drs,
235
+ });
236
+ }
237
+
238
+ function syncExternalTrackSurface(snapshot) {
239
+ if (!snapshot || typeof snapshot !== 'object') return;
240
+ const nextFingerprint = trackFingerprint(snapshot);
241
+ if (!nextFingerprint) return;
242
+ if (nextFingerprint === externalRendererTrackFingerprint) return;
243
+ if (
244
+ !app.trackAsset ||
245
+ !app.drsLayer ||
246
+ !app.sensorLayer ||
247
+ !app.pitLaneStatusLayer
248
+ ) {
249
+ externalRendererTrackFingerprint = nextFingerprint;
250
+ return;
251
+ }
252
+ app.pitLaneStatusRenderer?.reset?.();
253
+ app.cameraController?.invalidateTrackCaches?.();
254
+ renderTrackSurface({
255
+ drsLayer: app.drsLayer,
256
+ sensorLayer: app.sensorLayer,
257
+ pitLaneStatusLayer: app.pitLaneStatusLayer,
258
+ trackAsset: app.trackAsset,
259
+ snapshot,
260
+ });
261
+ externalRendererTrackFingerprint = nextFingerprint;
262
+ externalRendererAppliedTrackSurface = true;
263
+ }
264
+
265
+ const runtime = createEnvironmentRuntime({
12
266
  getSimulation: () => app.sim,
13
267
  setSimulation(nextSim) {
14
268
  app.sim = nextSim;
@@ -19,14 +273,118 @@ export function createBrowserExpertAdapter(app, expertOptions = {}) {
19
273
  },
20
274
  getOptions: () => resolvedOptions,
21
275
  setOptions(nextOptions) {
22
- resolvedOptions = nextOptions;
276
+ resolvedOptions = nextOptions.result?.stateOutput === 'none'
277
+ ? { ...nextOptions, result: { ...nextOptions.result, stateOutput: 'minimal' } }
278
+ : nextOptions;
23
279
  },
24
280
  afterReset(result) {
25
281
  app.renderTrack();
26
- app.renderExpertFrame(result.state.snapshot, { observation: result.observation });
282
+ renderExpertResult(result, { forceDomUpdate: true });
27
283
  },
28
284
  afterStep(result) {
29
- app.renderExpertFrame(result.state.snapshot, { observation: result.observation });
285
+ if (frameRenderSuppressed) return;
286
+ renderExpertResult(result);
30
287
  },
31
288
  });
289
+
290
+ function assertExternalRendererDetached(method) {
291
+ if (!externalRendererAttached) return;
292
+ throw new Error(`Browser expert ${method}() is disabled while external renderer mode is attached.`);
293
+ }
294
+
295
+ function detachExternalRenderer() {
296
+ if (typeof externalRendererUnsubscribe === 'function') {
297
+ try {
298
+ externalRendererUnsubscribe();
299
+ } catch (error) {
300
+ externalRendererLastError = error instanceof Error ? error.message : String(error);
301
+ }
302
+ }
303
+ externalRendererUnsubscribe = null;
304
+ externalRendererAttached = false;
305
+ externalRendererDriverIdMap = new Map();
306
+ if (externalRendererAppliedTrackSurface && typeof app.renderTrack === 'function') {
307
+ try {
308
+ app.renderTrack();
309
+ } catch (error) {
310
+ externalRendererLastError = error instanceof Error ? error.message : String(error);
311
+ }
312
+ }
313
+ externalRendererTrackFingerprint = null;
314
+ externalRendererAppliedTrackSurface = false;
315
+ }
316
+
317
+ function attachExternalRenderer(source) {
318
+ if (!source || typeof source.subscribe !== 'function') {
319
+ throw new Error('attachExternalRenderer(source) requires a source with subscribe(onFrame).');
320
+ }
321
+ detachExternalRenderer();
322
+ externalRendererLastError = null;
323
+ const unsubscribe = source.subscribe((frame) => {
324
+ if (!frame || typeof frame !== 'object') {
325
+ externalRendererLastError = 'Invalid external frame payload.';
326
+ return;
327
+ }
328
+ const rewrittenFrame = rewriteExternalFrame(frame);
329
+ const snapshot = rewrittenFrame.snapshot;
330
+ if (!snapshot || typeof snapshot !== 'object') {
331
+ externalRendererLastError = 'External frame is missing snapshot.';
332
+ return;
333
+ }
334
+ try {
335
+ syncExternalTrackSurface(snapshot);
336
+ app.renderExpertFrame(snapshot, {
337
+ forceDomUpdate: true,
338
+ observation: rewrittenFrame.observation && typeof rewrittenFrame.observation === 'object'
339
+ ? rewrittenFrame.observation
340
+ : {},
341
+ });
342
+ externalRendererLastMeta = rewrittenFrame.meta ?? null;
343
+ externalRendererLastFrameAt = Date.now();
344
+ externalRendererLastError = null;
345
+ } catch (error) {
346
+ externalRendererLastError = error instanceof Error ? error.message : String(error);
347
+ }
348
+ });
349
+ if (typeof unsubscribe !== 'function') {
350
+ throw new Error('attachExternalRenderer(source) subscribe(onFrame) must return an unsubscribe function.');
351
+ }
352
+ externalRendererUnsubscribe = unsubscribe;
353
+ externalRendererAttached = true;
354
+ }
355
+
356
+ function getExternalRendererState() {
357
+ return {
358
+ attached: externalRendererAttached,
359
+ lastMeta: externalRendererLastMeta,
360
+ lastFrameAt: externalRendererLastFrameAt,
361
+ lastError: externalRendererLastError,
362
+ };
363
+ }
364
+
365
+ return {
366
+ ...runtime,
367
+ reset(options = {}) {
368
+ assertExternalRendererDetached('reset');
369
+ return runtime.reset(options);
370
+ },
371
+ step(actions = {}) {
372
+ assertExternalRendererDetached('step');
373
+ return runtime.step(actions);
374
+ },
375
+ resetDrivers(placements = {}, resultOptions = {}) {
376
+ assertExternalRendererDetached('resetDrivers');
377
+ return runtime.resetDrivers(placements, resultOptions);
378
+ },
379
+ destroy() {
380
+ detachExternalRenderer();
381
+ runtime.destroy();
382
+ },
383
+ setFrameRenderSuppressed(suppressed) {
384
+ frameRenderSuppressed = Boolean(suppressed);
385
+ },
386
+ attachExternalRenderer,
387
+ detachExternalRenderer,
388
+ getExternalRendererState,
389
+ };
32
390
  }