@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.
- package/CHANGELOG.md +91 -0
- package/INSTALL_AND_UPDATE.md +5 -2
- package/README.md +328 -13
- package/docs/architecture.md +150 -41
- package/docs/concepts.md +22 -3
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +474 -34
- package/docs/data_model.md +1 -1
- package/docs/index.md +1 -0
- package/docs/learnings.md +23 -1
- package/docs/rules.md +169 -21
- package/docs/system_specs.md +56 -20
- package/docs/training.md +369 -25
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/examples/train-basic-policy.mjs +5 -6
- package/examples/trainingData.mjs +133 -0
- package/package.json +15 -4
- package/src/api/PaddockSimulatorController.js +36 -2
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +615 -940
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +14 -1
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +24 -4
- package/src/environment/actions.js +38 -7
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +206 -0
- package/src/environment/index.d.ts +638 -6
- package/src/environment/index.js +14 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +33 -0
- package/src/environment/observations.js +500 -43
- package/src/environment/options.js +41 -2
- package/src/environment/recorder.js +28 -0
- package/src/environment/runtime.js +260 -24
- package/src/environment/scenarios.js +249 -0
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +65 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -257
- package/src/environment/specs.js +188 -9
- package/src/environment/workerProtocol.js +64 -0
- package/src/index.d.ts +533 -7
- package/src/index.js +26 -0
- package/src/rendering/proceduralTrackAsset.js +20 -510
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +214 -0
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -253
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +92 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +386 -1012
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/collisionSteward.js +69 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyLedger.js +153 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/pitLaneSpeedingSteward.js +71 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +141 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/tireRequirementSteward.js +29 -0
- package/src/simulation/rules/trackLimitsSteward.js +85 -0
- package/src/simulation/rulesConfig.js +34 -0
- package/src/simulation/snapshots/raceSnapshots.js +192 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryIndex.js +945 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -628
- package/src/simulation/units.js +6 -4
- package/src/simulation/vehicle/contactResolution.js +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -0
- package/src/simulation/vehicleGeometry.js +1 -0
- package/src/simulation/vehiclePhysics.js +1 -104
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -0
- package/src/styles.css +342 -42
- package/src/ui/bannerTemplates.js +69 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +20 -426
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlStatusBanner.js +39 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +6 -5
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/templateUtils.js +23 -0
- 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 =
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
renderExpertResult(result, { forceDomUpdate: true });
|
|
27
283
|
},
|
|
28
284
|
afterStep(result) {
|
|
29
|
-
|
|
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
|
}
|