@inventure71/paddockjs 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/INSTALL_AND_UPDATE.md +23 -4
- package/README.md +31 -2
- package/docs/advanced_ai_pace.md +131 -0
- package/docs/advanced_physics_rebuild.md +166 -0
- package/docs/architecture.md +109 -30
- package/docs/cleanup_coverage.md +284 -0
- package/docs/component_inventory.md +31 -15
- package/docs/composable-layouts.md +2 -0
- package/docs/concepts.md +12 -1
- package/docs/custom_model_controller.md +1 -1
- package/docs/data_contract.md +33 -10
- package/docs/data_model.md +4 -2
- package/docs/efficiency_cleanup_2026-10-13.md +80 -0
- package/docs/grid_walk.md +80 -0
- package/docs/index.md +1 -0
- package/docs/keyboard_driving_fix.md +118 -0
- package/docs/limit_driving_2026-10-14.md +88 -0
- package/docs/maintainability_audit_2026-09-08.md +146 -0
- package/docs/racing_ai_improvements.md +145 -0
- package/docs/rules.md +43 -13
- package/docs/sense_contract.md +4 -1
- package/docs/system_specs.md +36 -11
- package/docs/testing-reliability-guide.md +5 -0
- package/docs/v11_improvements.md +108 -0
- package/package.json +23 -13
- package/src/api/MountedSimulatorComponents.js +101 -0
- package/src/api/PaddockSimulatorController.js +56 -81
- package/src/app/BrowserExpertAdapter.js +0 -6
- package/src/app/BrowserExpertExternalRenderer.js +72 -50
- package/src/app/ExternalTrackSnapshot.js +50 -0
- package/src/app/F1SimulatorApp.js +242 -297
- package/src/app/HostLifecycleEmitter.js +93 -0
- package/src/app/banners/raceDataBannerController.js +9 -4
- package/src/app/camera/cameraConstants.js +1 -1
- package/src/app/camera/cameraController.js +10 -13
- package/src/app/camera/cameraGeometry.js +1 -1
- package/src/app/controls/SimulatorViewControls.js +45 -0
- package/src/app/controls/workbenchBindings.js +64 -0
- package/src/app/controls/workbenchDisclosure.js +41 -0
- package/src/app/domBindings.js +21 -13
- package/src/app/gridWalk/GridWalkController.js +180 -0
- package/src/app/gridWalk/gridWalkCamera.js +88 -0
- package/src/app/gridWalk/gridWalkControls.js +24 -0
- package/src/app/gridWalk/gridWalkCrew.js +246 -0
- package/src/app/gridWalk/gridWalkLabels.js +104 -0
- package/src/app/gridWalk/gridWalkLayout.js +46 -0
- package/src/app/gridWalk/gridWalkNavigation.js +99 -0
- package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
- package/src/app/gridWalk/gridWalkSession.js +50 -0
- package/src/app/layoutSupport.js +2 -0
- package/src/app/raceOverlayClearanceSupport.js +21 -7
- package/src/app/readouts/carOverviewRenderer.js +9 -4
- package/src/app/readouts/readoutFormatters.js +1 -8
- package/src/app/readouts/telemetryRenderer.js +1 -3
- package/src/app/readouts/timingTowerRenderer.js +48 -50
- package/src/app/rendering/appAssets.js +8 -10
- package/src/app/rendering/carRenderer.js +27 -79
- package/src/app/rendering/displayUtils.js +0 -6
- package/src/app/rendering/expertSensorRenderer.js +1 -1
- package/src/app/rendering/replayGhostRenderer.js +4 -1
- package/src/app/rendering/trackRenderer.js +2 -2
- package/src/app/runtime/frameLoop.js +18 -17
- package/src/app/runtime/pixiStartupLifecycle.js +94 -0
- package/src/app/runtime/runtimeTiming.js +4 -1
- package/src/app/runtime/runtimeVisibility.js +7 -2
- package/src/config/cssValues.js +1 -1
- package/src/config/defaultOptions.js +12 -173
- package/src/config/restartOptions.js +1 -0
- package/src/config/runtimeSeed.js +10 -0
- package/src/config/simulatorDefaults.js +106 -0
- package/src/config/telemetryModules.js +20 -0
- package/src/config/theme/cssTokens.js +85 -0
- package/src/config/theme/themeCss.js +72 -0
- package/src/config/theme/themeInputs.js +155 -0
- package/src/config/theme/themeResolver.js +172 -0
- package/src/config/theme/themeSelectors.js +69 -0
- package/src/config/theme/themeTokens.js +257 -0
- package/src/config/theme/themeUtils.js +15 -0
- package/src/config/themeOptions.js +14 -830
- package/src/config/timingGapMode.js +0 -2
- package/src/config/uiOptions.js +53 -0
- package/src/data/championship.js +16 -158
- package/src/data/championshipDefaults.js +87 -0
- package/src/data/driverData.js +10 -20
- package/src/data/entryIdentity.js +67 -0
- package/src/data/entryPresentation.js +23 -0
- package/src/data/entryTypes.d.ts +160 -0
- package/src/data/index.d.ts +13 -259
- package/src/data/ratingScale.js +14 -0
- package/src/data/teamTypes.d.ts +23 -0
- package/src/data/trackTypes.d.ts +101 -0
- package/src/data/vehicleData.js +12 -23
- package/src/environment/actions.js +3 -2
- package/src/environment/controllerLoop.js +57 -44
- package/src/environment/controllerScheduler.js +30 -0
- package/src/environment/driverObservations.js +260 -0
- package/src/environment/driverReset.js +51 -0
- package/src/environment/episode.js +0 -1
- package/src/environment/evaluation.js +6 -90
- package/src/environment/evaluationTracker.js +100 -0
- package/src/environment/events.js +23 -0
- package/src/environment/index.d.ts +21 -140
- package/src/environment/metrics.js +3 -17
- package/src/environment/observationSchema.js +104 -0
- package/src/environment/observationVector.js +16 -115
- package/src/environment/observations.js +16 -316
- package/src/environment/options.js +5 -24
- package/src/environment/renderFrames.js +21 -0
- package/src/environment/resetOptions.js +41 -0
- package/src/environment/results.js +125 -0
- package/src/environment/rewards.js +10 -16
- package/src/environment/runtime.js +17 -300
- package/src/environment/scenarioOptions.js +90 -0
- package/src/environment/scenarioPlacements.js +70 -0
- package/src/environment/scenarioPresets.js +78 -0
- package/src/environment/scenarios.js +5 -236
- package/src/environment/scratch.js +18 -0
- package/src/environment/sensorOptions.js +20 -0
- package/src/environment/sensors/index.js +2 -556
- package/src/environment/sensors/indexedRayBands.js +0 -18
- package/src/environment/sensors/nearbyCars.js +18 -15
- package/src/environment/sensors/opponentRadar.js +3 -2
- package/src/environment/sensors/pitConnectorProximity.js +20 -0
- package/src/environment/sensors/rayBandTrace.js +1 -37
- package/src/environment/sensors/rayBatch.js +81 -0
- package/src/environment/sensors/rayConfig.js +26 -0
- package/src/environment/sensors/rayGeometry.js +0 -8
- package/src/environment/sensors/rayGuards.js +0 -6
- package/src/environment/sensors/rayOutputs.js +75 -0
- package/src/environment/sensors/rayQuery.js +110 -0
- package/src/environment/sensors/rayScratch.js +146 -0
- package/src/environment/sensors/sensorTargets.js +12 -4
- package/src/environment/sensors/surfaceRays.js +6 -61
- package/src/environment/sensors/trackRays.js +7 -80
- package/src/environment/simulationSetup.js +30 -0
- package/src/environment/specs.js +15 -109
- package/src/index.d.ts +34 -259
- package/src/index.js +28 -6
- package/src/placeholder/index.js +1 -8
- package/src/rendering/displayLifecycle.js +6 -0
- package/src/rendering/proceduralTrackAsset.js +1 -1
- package/src/rendering/renderSnapshot.js +3 -0
- package/src/rendering/track/finishLineRenderer.js +1 -1
- package/src/rendering/track/gridRenderer.js +1 -1
- package/src/rendering/track/offsetStrokeSafety.js +2 -2
- package/src/rendering/track/pitLaneRenderer.js +1 -1
- package/src/rendering/track/trackMaterialRenderer.js +4 -4
- package/src/rendering/track/trackRenderConstants.js +0 -1
- package/src/rendering/track/trackRenderGeometry.js +1 -7
- package/src/simulation/collisionGeometry.js +5 -11
- package/src/simulation/driver/advancedPathControls.js +53 -0
- package/src/simulation/driver/advancedRacePace.js +112 -0
- package/src/simulation/driver/advancedRacingControls.js +61 -0
- package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
- package/src/simulation/driver/advancedRacingSteering.js +76 -0
- package/src/simulation/driver/advancedRacingTiming.js +16 -0
- package/src/simulation/driver/arcadeRacePace.js +83 -0
- package/src/simulation/driver/arcadeRacingControls.js +111 -0
- package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
- package/src/simulation/driver/driverControlConstants.js +0 -2
- package/src/simulation/driver/driverController.js +14 -3
- package/src/simulation/driver/edgeRecovery.js +0 -1
- package/src/simulation/driver/racingControls.js +10 -406
- package/src/simulation/driver/racingDriverProfile.js +48 -0
- package/src/simulation/driver/racingLinePlan.js +48 -15
- package/src/simulation/driver/rejoinControls.js +58 -111
- package/src/simulation/driver/safetyCarControls.js +12 -0
- package/src/simulation/driver/trafficScan.js +89 -6
- package/src/simulation/pit/pitCarPlacement.js +29 -0
- package/src/simulation/pit/pitFlow.js +8 -30
- package/src/simulation/pit/pitIntent.js +4 -3
- package/src/simulation/pit/pitOccupancy.js +2 -2
- package/src/simulation/pit/pitQueue.js +2 -18
- package/src/simulation/pit/pitRouteMovement.js +1 -36
- package/src/simulation/pit/pitRouting.js +5 -81
- package/src/simulation/pit/pitService.js +3 -32
- package/src/simulation/pit/pitServiceConstants.js +1 -0
- package/src/simulation/pit/pitServiceProgress.js +34 -0
- package/src/simulation/pit/pitSnapshots.js +8 -4
- package/src/simulation/pit/pitState.js +1 -5
- package/src/simulation/race/classification.js +1 -8
- package/src/simulation/race/raceFinish.js +3 -8
- package/src/simulation/race/raceOrder.js +2 -13
- package/src/simulation/race/raceProgress.js +6 -20
- package/src/simulation/race/raceSetup.js +1 -1
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/redFlag.js +2 -4
- package/src/simulation/race/safetyCar.js +3 -8
- package/src/simulation/race/stalledDnf.js +2 -4
- package/src/simulation/raceSimulation.js +6 -38
- package/src/simulation/replay/replayGhosts.js +44 -32
- package/src/simulation/rules/collisionSteward.js +0 -14
- package/src/simulation/rules/penaltyActions.js +37 -0
- package/src/simulation/rules/penaltyLedger.js +8 -1
- package/src/simulation/rules/rulesReview.js +1 -7
- package/src/simulation/simMath.js +4 -0
- package/src/simulation/snapshots/raceSnapshots.js +30 -11
- package/src/simulation/timing/drsState.js +8 -0
- package/src/simulation/timing/drsTiming.js +2 -4
- package/src/simulation/timing/lapTelemetry.js +7 -4
- package/src/simulation/timing/sectorPerformance.js +3 -3
- package/src/simulation/timing/timingLines.js +12 -6
- package/src/simulation/track/pitLaneAccess.js +2 -2
- package/src/simulation/track/pitLaneLayout.js +4 -135
- package/src/simulation/track/pitLaneModel.js +70 -0
- package/src/simulation/track/pitLaneState.js +0 -1
- package/src/simulation/track/pitServiceLayout.js +121 -0
- package/src/simulation/track/proceduralTrack.js +77 -0
- package/src/simulation/track/spatialQueries.js +1 -25
- package/src/simulation/track/trackBuilder.js +54 -0
- package/src/simulation/track/trackConstants.js +0 -22
- package/src/simulation/track/trackMath.js +4 -29
- package/src/simulation/track/trackModel.js +4 -241
- package/src/simulation/track/trackModelCache.js +105 -0
- package/src/simulation/track/trackNearestQueries.js +1218 -0
- package/src/simulation/track/trackQueryConstants.js +3 -0
- package/src/simulation/track/trackQueryConstruction.js +368 -0
- package/src/simulation/track/trackQueryIndex.js +12 -1933
- package/src/simulation/track/trackQuerySegments.js +174 -0
- package/src/simulation/track/trackQueryStats.js +12 -0
- package/src/simulation/track/trackRayQueries.js +165 -0
- package/src/simulation/vehicle/advancedContactResponse.js +89 -0
- package/src/simulation/vehicle/advancedTireForces.js +92 -0
- package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
- package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
- package/src/simulation/vehicle/contactResolution.js +20 -6
- package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
- package/src/simulation/vehicle/pitWheelSurface.js +0 -12
- package/src/simulation/vehicle/runoffResponse.js +2 -4
- package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
- package/src/simulation/vehicle/vehicleGeometry.js +0 -56
- package/src/simulation/vehicle/vehicleKinematics.js +10 -0
- package/src/simulation/vehicle/vehiclePhysics.js +35 -194
- package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
- package/src/simulation/vehicle/vehicleState.js +2 -1
- package/src/simulation/vehicle/wheelSurface.js +13 -432
- package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
- package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
- package/src/styles.css +173 -19
- package/src/ui/bannerTemplates.js +4 -2
- package/src/ui/cameraControlsTemplate.js +4 -2
- package/src/ui/gridWalkTemplate.js +29 -0
- package/src/ui/htmlEscaping.js +8 -0
- package/src/ui/shellTemplate.js +3 -0
- package/src/ui/telemetryDrawerTemplate.js +4 -2
- package/src/ui/telemetryTemplates.js +3 -22
- package/src/ui/templateUtils.js +2 -8
- package/src/ui/viewControlsTemplate.js +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inventure71/paddockjs",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A data-driven browser F1 simulator component for host websites.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -88,8 +88,10 @@
|
|
|
88
88
|
"test:fast": "node scripts/run-vitest.mjs",
|
|
89
89
|
"types:check": "tsc -p tsconfig.types.json --noEmit",
|
|
90
90
|
"docs:check": "node scripts/docs-check.mjs",
|
|
91
|
-
"check": "npm run docs:check && npm run test:fast && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run consumer:bundle-boundaries && npm run
|
|
92
|
-
"check:release": "npm run docs:check && npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run consumer:bundle-boundaries && npm run
|
|
91
|
+
"check": "npm run docs:check && npm run test:fast && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run consumer:bundle-boundaries && npm run site:ci && npm run browser:smoke:quick -- --skip-build && npm run demo:smoke -- --skip-build",
|
|
92
|
+
"check:release": "npm run docs:check && npm run audit:release && npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run consumer:bundle-boundaries && npm run site:ci && npm run browser:smoke:full -- --skip-build && npm run demo:smoke -- --skip-build",
|
|
93
|
+
"audit:release": "npm run tools:release:ci && npm audit --audit-level=high && npm --prefix site audit --audit-level=high && npm --prefix tools/release audit --audit-level=high",
|
|
94
|
+
"tools:release:ci": "npm --prefix tools/release ci",
|
|
93
95
|
"pack:dry": "npm pack --dry-run",
|
|
94
96
|
"consumer:smoke": "node scripts/consumer-package-smoke.mjs",
|
|
95
97
|
"consumer:bundle-boundaries": "node scripts/consumer-bundle-boundaries.mjs",
|
|
@@ -97,21 +99,29 @@
|
|
|
97
99
|
"browser:smoke:quick": "node smoke/browser-smoke.mjs --quick",
|
|
98
100
|
"browser:smoke:full": "node smoke/browser-smoke.mjs --full",
|
|
99
101
|
"benchmark:runtime": "node scripts/benchmark-runtime-efficiency.mjs",
|
|
100
|
-
"showcase:install": "npm
|
|
101
|
-
"showcase:dev": "npm
|
|
102
|
-
"showcase:build": "npm
|
|
103
|
-
"showcase:ci": "npm
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
102
|
+
"showcase:install": "npm run site:install",
|
|
103
|
+
"showcase:dev": "npm run site:dev",
|
|
104
|
+
"showcase:build": "npm run site:build",
|
|
105
|
+
"showcase:ci": "npm run site:ci",
|
|
106
|
+
"demo:install": "npm run site:install",
|
|
107
|
+
"demo:dev": "npm run site:dev",
|
|
108
|
+
"demo:build": "npm run site:build",
|
|
109
|
+
"demo:ci": "npm run site:ci",
|
|
110
|
+
"demo:smoke": "node smoke/demo-smoke.mjs",
|
|
111
|
+
"changeset": "node scripts/run-changeset.mjs",
|
|
112
|
+
"version-packages": "node scripts/run-changeset.mjs version",
|
|
113
|
+
"release": "npm publish --provenance",
|
|
114
|
+
"site:install": "npm --prefix site ci",
|
|
115
|
+
"site:dev": "npm --prefix site run dev",
|
|
116
|
+
"site:build": "npm --prefix site run check",
|
|
117
|
+
"site:ci": "npm --prefix site ci && npm --prefix site run check"
|
|
107
118
|
},
|
|
108
119
|
"dependencies": {
|
|
109
|
-
"pixi.js": "8.
|
|
120
|
+
"pixi.js": "8.20.1"
|
|
110
121
|
},
|
|
111
122
|
"devDependencies": {
|
|
112
|
-
"@changesets/cli": "2.29.7",
|
|
113
123
|
"playwright": "^1.57.0",
|
|
114
124
|
"typescript": "5.9.3",
|
|
115
|
-
"vitest": "4.1.
|
|
125
|
+
"vitest": "4.1.12"
|
|
116
126
|
}
|
|
117
127
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { installLayoutSupport } from '../app/layoutSupport.js';
|
|
2
|
+
import { applyPaddockThemeCssVariables } from '../config/defaultOptions.js';
|
|
3
|
+
import { formatCssUrl } from '../config/cssValues.js';
|
|
4
|
+
|
|
5
|
+
function assertMountTarget(root, label) {
|
|
6
|
+
if (!root || typeof root !== 'object' || !('innerHTML' in root)) {
|
|
7
|
+
throw new Error(`${label} requires a DOM element root.`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function setPackageCssVariables(root, assets, theme, context = {}) {
|
|
12
|
+
root.classList?.add?.('f1-sim-component');
|
|
13
|
+
root.style?.setProperty?.('--broadcast-panel-surface', formatCssUrl(assets.broadcastPanel));
|
|
14
|
+
applyPaddockThemeCssVariables(root, theme, context);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createCompositeRoot(getRoots, getOptions) {
|
|
18
|
+
return {
|
|
19
|
+
style: {
|
|
20
|
+
setProperty(name, value) {
|
|
21
|
+
getRoots().forEach((root) => root.style?.setProperty?.(name, value));
|
|
22
|
+
},
|
|
23
|
+
removeProperty(name) {
|
|
24
|
+
getRoots().forEach((root) => root.style?.removeProperty?.(name));
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
querySelector(selector) {
|
|
28
|
+
for (const root of getRoots()) {
|
|
29
|
+
const match = root.querySelector?.(selector);
|
|
30
|
+
if (match) return match;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
},
|
|
34
|
+
querySelectorAll(selector) {
|
|
35
|
+
return getRoots().flatMap((root) => [...(root.querySelectorAll?.(selector) ?? [])]);
|
|
36
|
+
},
|
|
37
|
+
setAttribute(name, value) {
|
|
38
|
+
getRoots().forEach((root) => root.setAttribute?.(name, value));
|
|
39
|
+
},
|
|
40
|
+
removeAttribute(name) {
|
|
41
|
+
getRoots().forEach((root) => root.removeAttribute?.(name));
|
|
42
|
+
},
|
|
43
|
+
applyCssVariables(context = {}) {
|
|
44
|
+
const options = getOptions();
|
|
45
|
+
getRoots().forEach((root) => setPackageCssVariables(root, options.assets, options.theme, context));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Owns mounted DOM surfaces, layout-support lifetimes and their composite query
|
|
51
|
+
// root. The simulator controller supplies runtime availability and options.
|
|
52
|
+
export class MountedSimulatorComponents {
|
|
53
|
+
constructor({ getOptions, canMount }) {
|
|
54
|
+
this.getOptions = getOptions;
|
|
55
|
+
this.canMount = canMount;
|
|
56
|
+
this.roots = new Map();
|
|
57
|
+
this.mountRenderers = new Map();
|
|
58
|
+
this.layoutSupportCleanups = new Map();
|
|
59
|
+
this.compositeRoot = createCompositeRoot(() => [...this.roots.values()], getOptions);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
mount(root, key, createMarkup) {
|
|
63
|
+
assertMountTarget(root, `mount ${key}`);
|
|
64
|
+
if (!this.canMount()) {
|
|
65
|
+
throw new Error('Mount PaddockJS components before calling start().');
|
|
66
|
+
}
|
|
67
|
+
const occupiedEntry = [...this.roots.entries()]
|
|
68
|
+
.find(([mountedKey, mountedRoot]) => mountedKey !== key && mountedRoot === root);
|
|
69
|
+
if (occupiedEntry) {
|
|
70
|
+
throw new Error(`Mount ${key} requires its own root; this root already hosts ${occupiedEntry[0]}.`);
|
|
71
|
+
}
|
|
72
|
+
const render = typeof createMarkup === 'function' ? createMarkup : () => createMarkup;
|
|
73
|
+
const previousRoot = this.roots.get(key);
|
|
74
|
+
this.layoutSupportCleanups.get(key)?.();
|
|
75
|
+
if (previousRoot && previousRoot !== root) previousRoot.innerHTML = '';
|
|
76
|
+
root.innerHTML = render();
|
|
77
|
+
setPackageCssVariables(root, this.getOptions().assets, this.getOptions().theme);
|
|
78
|
+
this.layoutSupportCleanups.set(key, installLayoutSupport(root));
|
|
79
|
+
this.roots.set(key, root);
|
|
80
|
+
this.mountRenderers.set(key, render);
|
|
81
|
+
return root;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
rerender() {
|
|
85
|
+
this.roots.forEach((root, key) => {
|
|
86
|
+
const render = this.mountRenderers.get(key);
|
|
87
|
+
if (render) root.innerHTML = render();
|
|
88
|
+
setPackageCssVariables(root, this.getOptions().assets, this.getOptions().theme);
|
|
89
|
+
this.layoutSupportCleanups.get(key)?.();
|
|
90
|
+
this.layoutSupportCleanups.set(key, installLayoutSupport(root));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
destroy() {
|
|
95
|
+
this.layoutSupportCleanups.forEach((cleanup) => cleanup());
|
|
96
|
+
this.layoutSupportCleanups.clear();
|
|
97
|
+
this.roots.forEach((root) => { root.innerHTML = ''; });
|
|
98
|
+
this.roots.clear();
|
|
99
|
+
this.mountRenderers.clear();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { createViewControlsMarkup } from '../ui/viewControlsTemplate.js';
|
|
1
2
|
import { F1SimulatorApp } from '../app/F1SimulatorApp.js';
|
|
2
|
-
import {
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
198
|
-
if (!
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
}
|