@inventure71/paddockjs 0.3.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 (202) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/INSTALL_AND_UPDATE.md +5 -2
  3. package/README.md +328 -13
  4. package/docs/architecture.md +150 -41
  5. package/docs/concepts.md +22 -3
  6. package/docs/custom_model_controller.md +329 -0
  7. package/docs/data_contract.md +474 -34
  8. package/docs/data_model.md +1 -1
  9. package/docs/index.md +1 -0
  10. package/docs/learnings.md +23 -1
  11. package/docs/rules.md +169 -21
  12. package/docs/system_specs.md +56 -20
  13. package/docs/training.md +369 -25
  14. package/examples/python/README.md +62 -0
  15. package/examples/python/base_policy_server.py +191 -0
  16. package/examples/python/requirements.txt +2 -0
  17. package/examples/train-basic-policy.mjs +5 -6
  18. package/examples/trainingData.mjs +133 -0
  19. package/package.json +15 -4
  20. package/src/api/PaddockSimulatorController.js +36 -2
  21. package/src/app/BrowserExpertAdapter.js +367 -9
  22. package/src/app/F1SimulatorApp.js +615 -940
  23. package/src/app/banners/bannerFormatters.js +55 -0
  24. package/src/app/banners/raceDataBannerController.js +141 -0
  25. package/src/app/banners/stewardMessageController.js +99 -0
  26. package/src/app/camera/cameraConstants.js +21 -0
  27. package/src/app/camera/cameraController.js +265 -0
  28. package/src/app/camera/cameraGeometry.js +176 -0
  29. package/src/app/domBindings.js +14 -1
  30. package/src/app/readouts/carOverviewRenderer.js +109 -0
  31. package/src/app/readouts/controlStateRenderer.js +37 -0
  32. package/src/app/readouts/raceStatusRenderer.js +133 -0
  33. package/src/app/readouts/readoutFormatters.js +58 -0
  34. package/src/app/readouts/telemetryRenderer.js +113 -0
  35. package/src/app/readouts/timingTowerRenderer.js +177 -0
  36. package/src/app/rendering/appAssets.js +38 -0
  37. package/src/app/rendering/carRenderer.js +286 -0
  38. package/src/app/rendering/displayUtils.js +22 -0
  39. package/src/app/rendering/drsTrailRenderer.js +90 -0
  40. package/src/app/rendering/expertSensorRenderer.js +126 -0
  41. package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
  42. package/src/app/rendering/replayGhostRenderer.js +124 -0
  43. package/src/app/rendering/trackRenderer.js +25 -0
  44. package/src/app/runtime/frameLoop.js +66 -0
  45. package/src/app/runtime/runtimeTiming.js +34 -0
  46. package/src/app/runtime/runtimeVisibility.js +45 -0
  47. package/src/config/defaultOptions.js +24 -4
  48. package/src/environment/actions.js +38 -7
  49. package/src/environment/controllerLoop.js +246 -0
  50. package/src/environment/episode.js +86 -2
  51. package/src/environment/evaluation.js +206 -0
  52. package/src/environment/index.d.ts +638 -6
  53. package/src/environment/index.js +14 -0
  54. package/src/environment/metrics.js +159 -0
  55. package/src/environment/observationOptions.js +33 -0
  56. package/src/environment/observations.js +500 -43
  57. package/src/environment/options.js +41 -2
  58. package/src/environment/recorder.js +28 -0
  59. package/src/environment/runtime.js +260 -24
  60. package/src/environment/scenarios.js +249 -0
  61. package/src/environment/sensors/bodySenses.js +23 -0
  62. package/src/environment/sensors/boundarySenses.js +17 -0
  63. package/src/environment/sensors/carRays.js +75 -0
  64. package/src/environment/sensors/contactSenses.js +32 -0
  65. package/src/environment/sensors/index.js +157 -0
  66. package/src/environment/sensors/indexedRayBands.js +103 -0
  67. package/src/environment/sensors/nearbyCars.js +52 -0
  68. package/src/environment/sensors/opponentRadar.js +65 -0
  69. package/src/environment/sensors/rayConfig.js +110 -0
  70. package/src/environment/sensors/rayDefaults.js +6 -0
  71. package/src/environment/sensors/rayGeometry.js +60 -0
  72. package/src/environment/sensors/rayGuards.js +19 -0
  73. package/src/environment/sensors/sensorTargets.js +60 -0
  74. package/src/environment/sensors/surfaceRays.js +296 -0
  75. package/src/environment/sensors/trackRays.js +261 -0
  76. package/src/environment/sensors.js +1 -257
  77. package/src/environment/specs.js +188 -9
  78. package/src/environment/workerProtocol.js +64 -0
  79. package/src/index.d.ts +533 -7
  80. package/src/index.js +26 -0
  81. package/src/rendering/proceduralTrackAsset.js +20 -510
  82. package/src/rendering/renderSnapshot.js +38 -12
  83. package/src/rendering/track/finishLineRenderer.js +34 -0
  84. package/src/rendering/track/gridRenderer.js +47 -0
  85. package/src/rendering/track/offsetStrokeSafety.js +136 -0
  86. package/src/rendering/track/pitLaneRenderer.js +248 -0
  87. package/src/rendering/track/trackMaterialRenderer.js +326 -0
  88. package/src/rendering/track/trackRenderConstants.js +38 -0
  89. package/src/rendering/track/trackRenderGeometry.js +94 -0
  90. package/src/simulation/collisionGeometry.js +214 -0
  91. package/src/simulation/driver/driverControlConstants.js +28 -0
  92. package/src/simulation/driver/driverController.js +47 -0
  93. package/src/simulation/driver/driverInput.js +28 -0
  94. package/src/simulation/driver/driverMath.js +6 -0
  95. package/src/simulation/driver/driverPersonality.js +25 -0
  96. package/src/simulation/driver/edgeRecovery.js +48 -0
  97. package/src/simulation/driver/racingControls.js +399 -0
  98. package/src/simulation/driver/racingLinePlan.js +166 -0
  99. package/src/simulation/driver/recoveryDynamics.js +38 -0
  100. package/src/simulation/driver/rejoinControls.js +213 -0
  101. package/src/simulation/driver/safetyCarControls.js +35 -0
  102. package/src/simulation/driver/trafficScan.js +51 -0
  103. package/src/simulation/driverController.js +4 -253
  104. package/src/simulation/participants/participantInteractions.js +120 -0
  105. package/src/simulation/pit/pitFlow.js +234 -0
  106. package/src/simulation/pit/pitIntent.js +92 -0
  107. package/src/simulation/pit/pitOccupancy.js +47 -0
  108. package/src/simulation/pit/pitPenaltyService.js +71 -0
  109. package/src/simulation/pit/pitQueue.js +55 -0
  110. package/src/simulation/pit/pitRouteMovement.js +160 -0
  111. package/src/simulation/pit/pitRouting.js +319 -0
  112. package/src/simulation/pit/pitService.js +41 -0
  113. package/src/simulation/pit/pitServiceConstants.js +21 -0
  114. package/src/simulation/pit/pitServiceProfile.js +45 -0
  115. package/src/simulation/pit/pitSnapshots.js +61 -0
  116. package/src/simulation/pit/pitState.js +113 -0
  117. package/src/simulation/pit/pitTireService.js +92 -0
  118. package/src/simulation/race/classification.js +104 -0
  119. package/src/simulation/race/gridPenalties.js +37 -0
  120. package/src/simulation/race/raceControlState.js +38 -0
  121. package/src/simulation/race/raceDistance.js +32 -0
  122. package/src/simulation/race/raceFinish.js +65 -0
  123. package/src/simulation/race/raceLifecycle.js +71 -0
  124. package/src/simulation/race/raceOrder.js +113 -0
  125. package/src/simulation/race/raceProgress.js +124 -0
  126. package/src/simulation/race/raceSetup.js +93 -0
  127. package/src/simulation/race/raceStep.js +80 -0
  128. package/src/simulation/race/redFlag.js +15 -0
  129. package/src/simulation/race/retirements.js +44 -0
  130. package/src/simulation/race/safetyCar.js +86 -0
  131. package/src/simulation/race/stalledDnf.js +86 -0
  132. package/src/simulation/raceSimulation.js +386 -1012
  133. package/src/simulation/replay/replayGhosts.js +128 -0
  134. package/src/simulation/rules/collisionSteward.js +69 -0
  135. package/src/simulation/rules/moduleConfig.js +72 -0
  136. package/src/simulation/rules/penaltyConfig.js +99 -0
  137. package/src/simulation/rules/penaltyEvents.js +18 -0
  138. package/src/simulation/rules/penaltyLedger.js +153 -0
  139. package/src/simulation/rules/penaltyStats.js +26 -0
  140. package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
  141. package/src/simulation/rules/ruleConfigMerge.js +41 -0
  142. package/src/simulation/rules/ruleDefaults.js +85 -0
  143. package/src/simulation/rules/rulesReview.js +141 -0
  144. package/src/simulation/rules/rulesetPresets.js +47 -0
  145. package/src/simulation/rules/tireRequirementSteward.js +29 -0
  146. package/src/simulation/rules/trackLimitsSteward.js +85 -0
  147. package/src/simulation/rulesConfig.js +34 -0
  148. package/src/simulation/snapshots/raceSnapshots.js +192 -0
  149. package/src/simulation/timing/drsTiming.js +87 -0
  150. package/src/simulation/timing/gapEstimation.js +38 -0
  151. package/src/simulation/timing/lapTelemetry.js +182 -0
  152. package/src/simulation/timing/raceTiming.js +15 -0
  153. package/src/simulation/timing/sectorPerformance.js +60 -0
  154. package/src/simulation/timing/timingConstants.js +5 -0
  155. package/src/simulation/timing/timingHistory.js +46 -0
  156. package/src/simulation/timing/timingLines.js +79 -0
  157. package/src/simulation/track/centerlineSpline.js +59 -0
  158. package/src/simulation/track/drsZones.js +67 -0
  159. package/src/simulation/track/pitLaneAccess.js +103 -0
  160. package/src/simulation/track/pitLaneLayout.js +395 -0
  161. package/src/simulation/track/pitLaneState.js +160 -0
  162. package/src/simulation/track/proceduralCenterline.js +237 -0
  163. package/src/simulation/track/sampleGeometry.js +49 -0
  164. package/src/simulation/track/spatialQueries.js +154 -0
  165. package/src/simulation/track/startStraight.js +141 -0
  166. package/src/simulation/track/trackConstants.js +109 -0
  167. package/src/simulation/track/trackGenerationOptions.js +283 -0
  168. package/src/simulation/track/trackMath.js +241 -0
  169. package/src/simulation/track/trackModel.js +240 -0
  170. package/src/simulation/track/trackQueryIndex.js +945 -0
  171. package/src/simulation/track/trackSectors.js +19 -0
  172. package/src/simulation/track/trackStatePolicy.js +24 -0
  173. package/src/simulation/track/trackValidation.js +98 -0
  174. package/src/simulation/trackModel.js +1 -628
  175. package/src/simulation/units.js +6 -4
  176. package/src/simulation/vehicle/contactResolution.js +116 -0
  177. package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
  178. package/src/simulation/vehicle/pitWheelSurface.js +69 -0
  179. package/src/simulation/vehicle/runoffResponse.js +88 -0
  180. package/src/simulation/vehicle/surfacePriority.js +28 -0
  181. package/src/simulation/vehicle/vehicleGeometry.js +247 -0
  182. package/src/simulation/vehicle/vehiclePhysics.js +363 -0
  183. package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
  184. package/src/simulation/vehicle/vehicleState.js +186 -0
  185. package/src/simulation/vehicle/wheelSurface.js +150 -0
  186. package/src/simulation/vehicleGeometry.js +1 -0
  187. package/src/simulation/vehiclePhysics.js +1 -104
  188. package/src/simulation/warmup/runtimeWarmup.js +224 -0
  189. package/src/simulation/wheelSurface.js +1 -0
  190. package/src/styles.css +342 -42
  191. package/src/ui/bannerTemplates.js +69 -0
  192. package/src/ui/cameraControlsTemplate.js +20 -0
  193. package/src/ui/carOverviewTemplate.js +33 -0
  194. package/src/ui/componentTemplates.js +20 -426
  195. package/src/ui/raceCanvasTemplate.js +69 -0
  196. package/src/ui/raceControlStatusBanner.js +39 -0
  197. package/src/ui/raceControlsTemplate.js +36 -0
  198. package/src/ui/shellTemplate.js +6 -5
  199. package/src/ui/telemetryDrawerTemplate.js +55 -0
  200. package/src/ui/telemetryTemplates.js +136 -0
  201. package/src/ui/templateUtils.js +23 -0
  202. package/src/ui/timingTowerTemplate.js +34 -0
@@ -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
  }