@inventure71/paddockjs 3.0.0 → 4.1.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 +152 -1
- package/INSTALL_AND_UPDATE.md +21 -4
- package/README.md +103 -551
- package/docs/architecture.md +28 -18
- package/docs/component_inventory.md +5 -4
- package/docs/composable-layouts.md +79 -0
- package/docs/custom_model_controller.md +4 -4
- package/docs/data-helpers.md +50 -0
- package/docs/data_contract.md +13 -7
- package/docs/getting-started.md +105 -0
- package/docs/headless-environment.md +68 -0
- package/docs/index.md +17 -4
- package/docs/rules.md +6 -2
- package/docs/system_specs.md +60 -7
- package/docs/theming.md +64 -0
- package/docs/training.md +20 -14
- package/docs/troubleshooting.md +76 -0
- package/docs/upgrading-hosts-to-3.md +42 -0
- package/examples/python/README.md +6 -0
- package/examples/python/base_policy_server.py +19 -0
- package/package.json +21 -5
- package/src/api/PaddockSimulatorController.js +78 -22
- package/src/api/runtimeTheme.js +59 -0
- package/src/app/F1SimulatorApp.js +24 -3
- package/src/app/readouts/timingTowerRenderer.js +203 -42
- package/src/app/runtime/frameLoop.js +5 -1
- package/src/config/defaultOptions.js +8 -1
- package/src/config/themeOptions.js +5 -0
- package/src/data/index.d.ts +212 -0
- package/src/data/index.js +14 -0
- package/src/environment/episode.js +11 -1
- package/src/environment/metrics.js +59 -16
- package/src/environment/observationVector.js +50 -33
- package/src/environment/observations.js +89 -20
- package/src/environment/runtime.js +41 -0
- package/src/environment/sensors/carRays.js +48 -37
- package/src/environment/sensors/index.js +501 -97
- package/src/environment/sensors/indexedRayBands.js +134 -46
- package/src/environment/sensors/rayBandTrace.js +1171 -0
- package/src/environment/sensors/rayChannels.js +108 -0
- package/src/environment/sensors/rayConfig.js +71 -25
- package/src/environment/sensors/rayGeometry.js +23 -17
- package/src/environment/sensors/sensorTargets.js +67 -1
- package/src/environment/sensors/surfaceRays.js +130 -89
- package/src/environment/sensors/trackRays.js +115 -1
- package/src/index.d.ts +43 -0
- package/src/index.js +27 -2
- package/src/placeholder/index.d.ts +11 -0
- package/src/placeholder/index.js +61 -0
- package/src/placeholder.css +72 -0
- package/src/simulation/collisionGeometry.js +260 -96
- package/src/simulation/pit/pitFlow.js +21 -9
- package/src/simulation/pit/pitQueue.js +5 -3
- package/src/simulation/pit/pitRouteMovement.js +9 -4
- package/src/simulation/pit/pitRouting.js +77 -42
- package/src/simulation/pit/pitState.js +1 -0
- package/src/simulation/pit/pitTireService.js +7 -3
- package/src/simulation/race/raceFinish.js +25 -8
- package/src/simulation/race/raceOrder.js +189 -32
- package/src/simulation/race/raceProgress.js +129 -37
- package/src/simulation/race/raceSetup.js +2 -0
- package/src/simulation/race/raceStep.js +25 -13
- package/src/simulation/raceSimulation.js +20 -17
- package/src/simulation/rules/collisionSteward.js +40 -23
- package/src/simulation/rules/rulesReview.js +77 -56
- package/src/simulation/snapshots/raceSnapshots.js +37 -0
- package/src/simulation/timing/drsTiming.js +143 -30
- package/src/simulation/timing/gapEstimation.js +25 -3
- package/src/simulation/timing/lapTelemetry.js +115 -36
- package/src/simulation/timing/raceTiming.js +5 -1
- package/src/simulation/timing/sectorPerformance.js +104 -26
- package/src/simulation/timing/timingHistory.js +229 -14
- package/src/simulation/timing/timingLines.js +65 -7
- package/src/simulation/track/pitLaneLayout.js +23 -0
- package/src/simulation/track/pitLaneState.js +218 -19
- package/src/simulation/track/spatialQueries.js +67 -11
- package/src/simulation/track/trackMath.js +91 -24
- package/src/simulation/track/trackModel.js +2 -0
- package/src/simulation/track/trackQueryIndex.js +1334 -100
- package/src/simulation/track/trackQueryPitIndex.js +138 -14
- package/src/simulation/track/trackQueryScratch.js +7 -0
- package/src/simulation/track/trackQueryStats.js +25 -0
- package/src/simulation/track/trackSnapshotJson.js +139 -0
- package/src/simulation/track/trackStatePolicy.js +249 -1
- package/src/simulation/vehicle/contactResolution.js +96 -59
- package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
- package/src/simulation/vehicle/pitWheelSurface.js +81 -28
- package/src/simulation/vehicle/runoffResponse.js +54 -29
- package/src/simulation/vehicle/vehicleGeometry.js +245 -51
- package/src/simulation/vehicle/vehiclePhysics.js +42 -27
- package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
- package/src/simulation/vehicle/vehicleState.js +6 -0
- package/src/simulation/vehicle/wheelSurface.js +807 -92
- package/src/styles.css +497 -497
- package/src/ui/raceControlsTemplate.js +1 -0
- package/src/ui/telemetryDrawerTemplate.js +1 -0
- package/src/ui/telemetryTemplates.js +1 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Use this when a host integration or package release check fails.
|
|
4
|
+
|
|
5
|
+
## Raw Node Import Fails
|
|
6
|
+
|
|
7
|
+
Do not use this as the main package check:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
node -e "import('@inventure71/paddockjs')"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The root package is browser-oriented and imports CSS/assets. Use the browser-free subpaths in Node:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
|
|
17
|
+
import { DriverData } from '@inventure71/paddockjs/data';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## CSS Is Missing
|
|
21
|
+
|
|
22
|
+
Import the package stylesheet from a browser entry when the host bundler does not extract it automatically:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import '@inventure71/paddockjs/styles.css';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If styles are present but the simulator is squeezed or overlapping, check the host container size before changing package internals. PaddockJS supports roots at least `320px` wide and shows an `Unsupported size` placeholder below supported bounds.
|
|
29
|
+
|
|
30
|
+
## Assets Do Not Resolve
|
|
31
|
+
|
|
32
|
+
PaddockJS owns default simulator assets. Hosts should not copy them manually. In the package repo, verify publish contents with:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run pack:dry
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
In a host app, verify through the host's browser build rather than a raw Node import.
|
|
39
|
+
|
|
40
|
+
## Simulator Looks Frozen
|
|
41
|
+
|
|
42
|
+
Check whether browser expert mode is enabled:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
expert: { enabled: true, controlledDrivers: ['budget'] }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Expert mode intentionally waits for `simulator.expert.step(actions)`. Basic website mounts should omit `expert` unless they are playing back a model or debugging policy observations.
|
|
49
|
+
|
|
50
|
+
## Policy Server Receives No Observations
|
|
51
|
+
|
|
52
|
+
Policy Runner server mode uses protocol version `2`. `/policy/decide-batch`
|
|
53
|
+
receives aligned compact arrays, not rich `observations`. Static specs,
|
|
54
|
+
configuration, `previousActionFields`, and `metricFields` are sent once to
|
|
55
|
+
`/policy/reset`, so servers should cache `observationSpec` and read model
|
|
56
|
+
inputs from `body.vectors[index]` for `body.driverIds[index]` during decisions.
|
|
57
|
+
|
|
58
|
+
## Theme Toggle Resets The Race
|
|
59
|
+
|
|
60
|
+
Use runtime theme APIs instead of restarting:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
simulator.setThemeMode('dark');
|
|
64
|
+
simulator.setTheme({ tokens: { primary: '#008c55' } });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`restart(nextOptions)` is for race/data/seed changes, not normal light/dark presentation sync.
|
|
68
|
+
|
|
69
|
+
## 3.x Upgrade Symptoms
|
|
70
|
+
|
|
71
|
+
- Remove host usage of `trackQueryIndex`; indexed track queries are package internals.
|
|
72
|
+
- Replace old strict physics usage with `physicsMode: 'advanced'`, or omit `physicsMode` for the default `arcade` mode.
|
|
73
|
+
- Use `@inventure71/paddockjs/environment` for headless simulation.
|
|
74
|
+
- Use `@inventure71/paddockjs/data` for CSS-free data/tooling imports.
|
|
75
|
+
|
|
76
|
+
See [Upgrading Hosts To PaddockJS 3.x](upgrading-hosts-to-3.md).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Upgrading Hosts To PaddockJS 4.x
|
|
2
|
+
|
|
3
|
+
Use this checklist when moving a browser host website from 2.x-era PaddockJS usage to the 4.x package API.
|
|
4
|
+
|
|
5
|
+
1. Install the 4.x package:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @inventure71/paddockjs@4
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
2. Import package CSS intentionally anywhere browser mounts are used:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import '@inventure71/paddockjs/styles.css';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The stylesheet is scoped to PaddockJS roots and uses system fonts by default. Load any branded web fonts from the host app instead of relying on package-side remote font imports.
|
|
18
|
+
|
|
19
|
+
3. Remove host usage of `trackQueryIndex`. Track query indexing is package-owned and internal in 4.x.
|
|
20
|
+
|
|
21
|
+
4. Replace `physicsMode: 'simulator'` with `physicsMode: 'advanced'`, or omit `physicsMode` to keep the default `arcade` mode.
|
|
22
|
+
|
|
23
|
+
5. Decide how the host should theme the simulator. Use mount-time `theme` for defaults, then use `simulator.setThemeMode(mode)` or `simulator.setTheme(theme)` for site light/dark toggles. Do not call `restart({ theme })` only to change presentation.
|
|
24
|
+
|
|
25
|
+
6. Reuse public theme helpers instead of copying CSS variable names:
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import {
|
|
29
|
+
PADDOCK_THEME_CSS_VARIABLES,
|
|
30
|
+
PADDOCK_THEME_TOKEN_KEYS,
|
|
31
|
+
applyPaddockTheme,
|
|
32
|
+
resolvePaddockTheme,
|
|
33
|
+
} from '@inventure71/paddockjs';
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
7. Decide whether the host should enable the driver camera with `ui.driverCamera: true` or `initialCameraMode: 'driver'`.
|
|
37
|
+
|
|
38
|
+
8. Use `@inventure71/paddockjs/environment` for browser-free simulation/training imports.
|
|
39
|
+
|
|
40
|
+
9. Use `@inventure71/paddockjs/data` for CSS-free data/helper imports such as `DriverData`, `normalizeSimulatorDrivers`, `createProceduralTrack`, and speed conversion helpers.
|
|
41
|
+
|
|
42
|
+
10. Rebuild the host bundle and browser-smoke the host page, including theme toggles, selected-driver surfaces, and any composable mount roots.
|
|
@@ -14,6 +14,8 @@ It is not a trainer and not a model/checkpoint format owner. It only shows trans
|
|
|
14
14
|
- Preview latest-frame HTTP endpoint (for polling visualizers)
|
|
15
15
|
- `http://127.0.0.1:8787/preview/frame`
|
|
16
16
|
|
|
17
|
+
Policy HTTP uses protocol version `2`. `POST /policy/reset` receives static metadata (`actionSpec`, `observationSpec`, configuration) once per session. `POST /policy/decide-batch` is compact and receives `driverIds`, `vectors`, `previousActions`, `metrics`, and `events`; it does not receive rich observation objects, schemas, snapshots, or track metadata every decision.
|
|
18
|
+
|
|
17
19
|
## Run
|
|
18
20
|
|
|
19
21
|
```bash
|
|
@@ -55,6 +57,10 @@ Subclass `BasePolicyServer` and override:
|
|
|
55
57
|
|
|
56
58
|
Use `broadcast_preview_frame(snapshot, observation, meta)` to push authoritative frames to the browser renderer.
|
|
57
59
|
|
|
60
|
+
Inside `decide_batch(ctx)`, compact per-driver fields are aligned arrays:
|
|
61
|
+
`ctx["vectors"][index]` belongs to `ctx["driverIds"][index]`. The base example's
|
|
62
|
+
`compact_values_by_driver(ctx, "vectors")` helper maps them back to driver ids.
|
|
63
|
+
|
|
58
64
|
## Important Boundary
|
|
59
65
|
|
|
60
66
|
- This server does not step the simulator by itself.
|
|
@@ -10,6 +10,9 @@ This file is intentionally minimal and transport-focused:
|
|
|
10
10
|
- /preview (broadcasts {snapshot, observation, meta})
|
|
11
11
|
|
|
12
12
|
Extend `BasePolicyServer` and override hook methods to plug your own model/runtime.
|
|
13
|
+
Policy HTTP uses protocolVersion 2. Reset receives static specs/configuration;
|
|
14
|
+
decide-batch receives compact per-driver vectors instead of rich observations.
|
|
15
|
+
Compact per-driver fields are arrays aligned with driverIds.
|
|
13
16
|
"""
|
|
14
17
|
|
|
15
18
|
from __future__ import annotations
|
|
@@ -33,6 +36,20 @@ def _zero_action() -> Dict[str, float]:
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
|
|
39
|
+
def compact_values_by_driver(context: Mapping[str, Any], field: str) -> Dict[str, Any]:
|
|
40
|
+
"""Map a compact aligned field such as vectors/metrics back to driver ids."""
|
|
41
|
+
driver_ids = list(context.get("driverIds") or [])
|
|
42
|
+
values = context.get(field) or []
|
|
43
|
+
if isinstance(values, Mapping):
|
|
44
|
+
return {driver_id: values.get(driver_id) for driver_id in driver_ids}
|
|
45
|
+
if not isinstance(values, list):
|
|
46
|
+
return {driver_id: None for driver_id in driver_ids}
|
|
47
|
+
return {
|
|
48
|
+
driver_id: values[index] if index < len(values) else None
|
|
49
|
+
for index, driver_id in enumerate(driver_ids)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
36
53
|
@dataclass
|
|
37
54
|
class ServerState:
|
|
38
55
|
session_id: int = 0
|
|
@@ -70,6 +87,8 @@ class BasePolicyServer:
|
|
|
70
87
|
async def decide_batch(self, context: Mapping[str, Any]) -> Dict[str, Dict[str, float]]:
|
|
71
88
|
"""Return per-driver controls for one batched policy step."""
|
|
72
89
|
driver_ids = list(context.get("driverIds") or [])
|
|
90
|
+
vectors_by_driver = compact_values_by_driver(context, "vectors")
|
|
91
|
+
_ = vectors_by_driver
|
|
73
92
|
return {driver_id: _zero_action() for driver_id in driver_ids}
|
|
74
93
|
|
|
75
94
|
async def publish_preview_frame(self, frame: Mapping[str, Any]) -> None:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inventure71/paddockjs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A data-driven browser F1 simulator component for host websites.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"ui-component"
|
|
25
25
|
],
|
|
26
26
|
"sideEffects": [
|
|
27
|
-
"./src/styles.css"
|
|
27
|
+
"./src/styles.css",
|
|
28
|
+
"./src/placeholder.css"
|
|
28
29
|
],
|
|
29
30
|
"publishConfig": {
|
|
30
31
|
"access": "public"
|
|
@@ -45,7 +46,18 @@
|
|
|
45
46
|
"import": "./src/environment/index.js",
|
|
46
47
|
"default": "./src/environment/index.js"
|
|
47
48
|
},
|
|
48
|
-
"./
|
|
49
|
+
"./data": {
|
|
50
|
+
"types": "./src/data/index.d.ts",
|
|
51
|
+
"import": "./src/data/index.js",
|
|
52
|
+
"default": "./src/data/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./placeholder": {
|
|
55
|
+
"types": "./src/placeholder/index.d.ts",
|
|
56
|
+
"import": "./src/placeholder/index.js",
|
|
57
|
+
"default": "./src/placeholder/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./styles.css": "./src/styles.css",
|
|
60
|
+
"./placeholder.css": "./src/placeholder.css"
|
|
49
61
|
},
|
|
50
62
|
"files": [
|
|
51
63
|
"CHANGELOG.md",
|
|
@@ -63,25 +75,29 @@
|
|
|
63
75
|
"src/config",
|
|
64
76
|
"src/data",
|
|
65
77
|
"src/environment",
|
|
78
|
+
"src/placeholder",
|
|
66
79
|
"src/rendering",
|
|
67
80
|
"src/simulation",
|
|
68
81
|
"src/ui",
|
|
69
82
|
"src/index.d.ts",
|
|
70
83
|
"src/index.js",
|
|
71
84
|
"src/styles.css",
|
|
85
|
+
"src/placeholder.css",
|
|
72
86
|
"README.md"
|
|
73
87
|
],
|
|
74
88
|
"scripts": {
|
|
75
89
|
"test": "node scripts/run-vitest.mjs --slow",
|
|
76
90
|
"test:fast": "node scripts/run-vitest.mjs",
|
|
77
91
|
"types:check": "tsc -p tsconfig.types.json --noEmit",
|
|
78
|
-
"check": "
|
|
79
|
-
"check
|
|
92
|
+
"docs:check": "node scripts/docs-check.mjs",
|
|
93
|
+
"check": "npm run docs:check && npm run test:fast && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run showcase:ci && npm run browser:smoke:quick -- --skip-build",
|
|
94
|
+
"check:release": "npm run docs:check && npm test && npm run types:check && npm run pack:dry && npm run consumer:smoke && npm run showcase:ci && npm run browser:smoke:full -- --skip-build",
|
|
80
95
|
"pack:dry": "npm pack --dry-run",
|
|
81
96
|
"consumer:smoke": "node scripts/consumer-package-smoke.mjs",
|
|
82
97
|
"browser:smoke": "node smoke/browser-smoke.mjs",
|
|
83
98
|
"browser:smoke:quick": "node smoke/browser-smoke.mjs --quick",
|
|
84
99
|
"browser:smoke:full": "node smoke/browser-smoke.mjs --full",
|
|
100
|
+
"benchmark:runtime": "node scripts/benchmark-runtime-efficiency.mjs",
|
|
85
101
|
"showcase:install": "npm --prefix local-preview ci",
|
|
86
102
|
"showcase:dev": "npm --prefix local-preview run dev",
|
|
87
103
|
"showcase:build": "npm --prefix local-preview run build",
|
|
@@ -22,6 +22,7 @@ import { applyPaddockThemeCssVariables, resolveF1SimulatorOptions } from '../con
|
|
|
22
22
|
import { formatCssUrl } from '../config/cssValues.js';
|
|
23
23
|
import { mergeRestartOptions } from '../config/restartOptions.js';
|
|
24
24
|
import { getNextTimingGapMode, normalizeTimingGapMode } from '../config/timingGapMode.js';
|
|
25
|
+
import { createThemeSync, resolveRuntimeThemeModeOptions, resolveRuntimeThemeOptions } from './runtimeTheme.js';
|
|
25
26
|
|
|
26
27
|
function assertMountTarget(root, label) {
|
|
27
28
|
if (!root || typeof root !== 'object' || !('innerHTML' in root)) {
|
|
@@ -29,10 +30,10 @@ function assertMountTarget(root, label) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function setPackageCssVariables(root, assets, theme) {
|
|
33
|
+
function setPackageCssVariables(root, assets, theme, context = {}) {
|
|
33
34
|
root.classList?.add?.('f1-sim-component');
|
|
34
35
|
root.style?.setProperty?.('--broadcast-panel-surface', formatCssUrl(assets.broadcastPanel));
|
|
35
|
-
applyPaddockThemeCssVariables(root, theme);
|
|
36
|
+
applyPaddockThemeCssVariables(root, theme, context);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
function createCompositeRoot(getRoots, getOptions) {
|
|
@@ -41,6 +42,9 @@ function createCompositeRoot(getRoots, getOptions) {
|
|
|
41
42
|
setProperty(name, value) {
|
|
42
43
|
getRoots().forEach((root) => root.style?.setProperty?.(name, value));
|
|
43
44
|
},
|
|
45
|
+
removeProperty(name) {
|
|
46
|
+
getRoots().forEach((root) => root.style?.removeProperty?.(name));
|
|
47
|
+
},
|
|
44
48
|
},
|
|
45
49
|
querySelector(selector) {
|
|
46
50
|
for (const root of getRoots()) {
|
|
@@ -58,9 +62,9 @@ function createCompositeRoot(getRoots, getOptions) {
|
|
|
58
62
|
removeAttribute(name) {
|
|
59
63
|
getRoots().forEach((root) => root.removeAttribute?.(name));
|
|
60
64
|
},
|
|
61
|
-
applyCssVariables() {
|
|
65
|
+
applyCssVariables(context = {}) {
|
|
62
66
|
const options = getOptions();
|
|
63
|
-
getRoots().forEach((root) => setPackageCssVariables(root, options.assets, options.theme));
|
|
67
|
+
getRoots().forEach((root) => setPackageCssVariables(root, options.assets, options.theme, context));
|
|
64
68
|
},
|
|
65
69
|
};
|
|
66
70
|
}
|
|
@@ -69,38 +73,41 @@ export class PaddockSimulatorController {
|
|
|
69
73
|
constructor(options = {}) {
|
|
70
74
|
this.options = resolveF1SimulatorOptions(options);
|
|
71
75
|
this.roots = new Map();
|
|
76
|
+
this.mountRenderers = new Map();
|
|
72
77
|
this.layoutSupportCleanups = new Map();
|
|
73
78
|
this.app = null;
|
|
74
79
|
this.compositeRoot = createCompositeRoot(() => [...this.roots.values()], () => this.options);
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
mountComponent(root, key,
|
|
82
|
+
mountComponent(root, key, createMarkup) {
|
|
78
83
|
assertMountTarget(root, `mount ${key}`);
|
|
79
84
|
if (this.app) {
|
|
80
85
|
throw new Error('Mount PaddockJS components before calling start().');
|
|
81
86
|
}
|
|
82
|
-
|
|
87
|
+
const render = typeof createMarkup === 'function' ? createMarkup : () => createMarkup;
|
|
88
|
+
root.innerHTML = render();
|
|
83
89
|
setPackageCssVariables(root, this.options.assets, this.options.theme);
|
|
84
90
|
this.layoutSupportCleanups.get(key)?.();
|
|
85
91
|
this.layoutSupportCleanups.set(key, installLayoutSupport(root));
|
|
86
92
|
this.roots.set(key, root);
|
|
93
|
+
this.mountRenderers.set(key, render);
|
|
87
94
|
return root;
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
mountRaceControls(root) {
|
|
91
|
-
return this.mountComponent(root, 'race-controls', createRaceControlsMarkup(this.options));
|
|
98
|
+
return this.mountComponent(root, 'race-controls', () => createRaceControlsMarkup(this.options));
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
mountCameraControls(root) {
|
|
95
|
-
return this.mountComponent(root, 'camera-controls', createCameraControlsMarkup(this.options));
|
|
102
|
+
return this.mountComponent(root, 'camera-controls', () => createCameraControlsMarkup(this.options));
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
mountSafetyCarControl(root) {
|
|
99
|
-
return this.mountComponent(root, 'safety-car-control', createSafetyCarControlMarkup(this.options));
|
|
106
|
+
return this.mountComponent(root, 'safety-car-control', () => createSafetyCarControlMarkup(this.options));
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
mountTimingTower(root) {
|
|
103
|
-
return this.mountComponent(root, 'timing-tower', createTimingTowerMarkup(this.options));
|
|
110
|
+
return this.mountComponent(root, 'timing-tower', () => createTimingTowerMarkup(this.options));
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
mountRaceCanvas(root, {
|
|
@@ -110,7 +117,7 @@ export class PaddockSimulatorController {
|
|
|
110
117
|
timingTowerVerticalFit,
|
|
111
118
|
responsiveNarrowLayout,
|
|
112
119
|
} = {}) {
|
|
113
|
-
return this.mountComponent(root, 'race-canvas', createRaceCanvasMarkup({
|
|
120
|
+
return this.mountComponent(root, 'race-canvas', () => createRaceCanvasMarkup({
|
|
114
121
|
...this.options,
|
|
115
122
|
includeRaceDataPanel,
|
|
116
123
|
includeTimingTower,
|
|
@@ -121,39 +128,39 @@ export class PaddockSimulatorController {
|
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
mountTelemetryPanel(root, { includeOverview } = {}) {
|
|
124
|
-
return this.mountComponent(root, 'telemetry-stack', createTelemetryPanelMarkup(this.options, { includeOverview }));
|
|
131
|
+
return this.mountComponent(root, 'telemetry-stack', () => createTelemetryPanelMarkup(this.options, { includeOverview }));
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
mountTelemetryCore(root) {
|
|
128
|
-
return this.mountComponent(root, 'telemetry-core', createTelemetryCoreMarkup(this.options));
|
|
135
|
+
return this.mountComponent(root, 'telemetry-core', () => createTelemetryCoreMarkup(this.options));
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
mountTelemetrySectors(root) {
|
|
132
|
-
return this.mountComponent(root, 'telemetry-sectors', createTelemetrySectorsMarkup(this.options));
|
|
139
|
+
return this.mountComponent(root, 'telemetry-sectors', () => createTelemetrySectorsMarkup(this.options));
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
mountTelemetryLapTimes(root) {
|
|
136
|
-
return this.mountComponent(root, 'telemetry-lap-times', createTelemetryLapTimesMarkup(this.options));
|
|
143
|
+
return this.mountComponent(root, 'telemetry-lap-times', () => createTelemetryLapTimesMarkup(this.options));
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
mountTelemetrySectorTimes(root) {
|
|
140
|
-
return this.mountComponent(root, 'telemetry-sector-times', createTelemetrySectorTimesMarkup(this.options));
|
|
147
|
+
return this.mountComponent(root, 'telemetry-sector-times', () => createTelemetrySectorTimesMarkup(this.options));
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
mountTelemetrySectorBanner(root) {
|
|
144
|
-
return this.mountComponent(root, 'telemetry-sector-banner', createTelemetrySectorBannerMarkup(this.options));
|
|
151
|
+
return this.mountComponent(root, 'telemetry-sector-banner', () => createTelemetrySectorBannerMarkup(this.options));
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
mountRaceTelemetryDrawer(root, options = {}) {
|
|
148
|
-
return this.mountComponent(root, 'race-telemetry-drawer', createRaceTelemetryDrawerMarkup(this.options, options));
|
|
155
|
+
return this.mountComponent(root, 'race-telemetry-drawer', () => createRaceTelemetryDrawerMarkup(this.options, options));
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
mountCarDriverOverview(root) {
|
|
152
|
-
return this.mountComponent(root, 'car-driver-overview', createCarDriverOverviewMarkup(this.options));
|
|
159
|
+
return this.mountComponent(root, 'car-driver-overview', () => createCarDriverOverviewMarkup(this.options));
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
mountRaceDataPanel(root) {
|
|
156
|
-
return this.mountComponent(root, 'race-data-panel', createRaceDataPanelMarkup({
|
|
163
|
+
return this.mountComponent(root, 'race-data-panel', () => createRaceDataPanelMarkup({
|
|
157
164
|
...this.options,
|
|
158
165
|
standalone: true,
|
|
159
166
|
}));
|
|
@@ -188,6 +195,17 @@ export class PaddockSimulatorController {
|
|
|
188
195
|
root.innerHTML = '';
|
|
189
196
|
});
|
|
190
197
|
this.roots.clear();
|
|
198
|
+
this.mountRenderers.clear();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
rerenderMountedComponents() {
|
|
202
|
+
this.roots.forEach((root, key) => {
|
|
203
|
+
const render = this.mountRenderers.get(key);
|
|
204
|
+
if (render) root.innerHTML = render();
|
|
205
|
+
setPackageCssVariables(root, this.options.assets, this.options.theme);
|
|
206
|
+
this.layoutSupportCleanups.get(key)?.();
|
|
207
|
+
this.layoutSupportCleanups.set(key, installLayoutSupport(root));
|
|
208
|
+
});
|
|
191
209
|
}
|
|
192
210
|
|
|
193
211
|
syncOptionsFromRunningApp() {
|
|
@@ -207,12 +225,50 @@ export class PaddockSimulatorController {
|
|
|
207
225
|
const currentOptions = this.app ? this.syncOptionsFromRunningApp() : this.options;
|
|
208
226
|
const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(currentOptions, nextOptions));
|
|
209
227
|
if (this.app) {
|
|
210
|
-
this.
|
|
228
|
+
const previousOptions = this.options;
|
|
211
229
|
this.options = nextResolvedOptions;
|
|
230
|
+
try {
|
|
231
|
+
this.app.restart(nextResolvedOptions);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
this.options = previousOptions;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
212
236
|
return;
|
|
213
237
|
}
|
|
214
238
|
this.options = nextResolvedOptions;
|
|
215
|
-
this.
|
|
239
|
+
this.rerenderMountedComponents();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setTheme(themeInput = {}) {
|
|
243
|
+
const currentOptions = this.app ? this.syncOptionsFromRunningApp() : this.options;
|
|
244
|
+
const nextResolvedOptions = resolveRuntimeThemeOptions(currentOptions, themeInput);
|
|
245
|
+
this.options = nextResolvedOptions;
|
|
246
|
+
if (this.app?.setTheme) {
|
|
247
|
+
this.app.setTheme(nextResolvedOptions.theme);
|
|
248
|
+
} else {
|
|
249
|
+
this.compositeRoot.applyCssVariables();
|
|
250
|
+
}
|
|
251
|
+
return nextResolvedOptions.theme;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setThemeMode(mode) {
|
|
255
|
+
const currentOptions = this.app ? this.syncOptionsFromRunningApp() : this.options;
|
|
256
|
+
const nextResolvedOptions = resolveRuntimeThemeModeOptions(currentOptions, mode);
|
|
257
|
+
this.options = nextResolvedOptions;
|
|
258
|
+
if (this.app?.setTheme) {
|
|
259
|
+
this.app.setTheme(nextResolvedOptions.theme);
|
|
260
|
+
} else {
|
|
261
|
+
this.compositeRoot.applyCssVariables();
|
|
262
|
+
}
|
|
263
|
+
return nextResolvedOptions.theme;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getTheme() {
|
|
267
|
+
return this.options.theme;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
syncThemeFrom(source, options = {}) {
|
|
271
|
+
return createThemeSync(this, source, options);
|
|
216
272
|
}
|
|
217
273
|
|
|
218
274
|
selectDriver(driverId) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { resolveF1SimulatorOptions } from '../config/defaultOptions.js';
|
|
2
|
+
import { mergeThemeInputs } from '../config/themeOptions.js';
|
|
3
|
+
|
|
4
|
+
export function resolveRuntimeThemeOptions(currentOptions, themeInput = {}) {
|
|
5
|
+
return resolveF1SimulatorOptions({
|
|
6
|
+
...currentOptions,
|
|
7
|
+
theme: mergeThemeInputs(currentOptions?.theme, themeInput),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveRuntimeThemeModeOptions(currentOptions, mode) {
|
|
12
|
+
return resolveRuntimeThemeOptions(currentOptions, { mode });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createThemeSync(simulator, source, {
|
|
16
|
+
attribute = 'data-theme',
|
|
17
|
+
map,
|
|
18
|
+
} = {}) {
|
|
19
|
+
if (!source || typeof source !== 'object') {
|
|
20
|
+
throw new Error('syncThemeFrom requires a DOM element or attribute source.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const readValue = () => {
|
|
24
|
+
const rawValue = typeof source.getAttribute === 'function'
|
|
25
|
+
? source.getAttribute(attribute)
|
|
26
|
+
: source?.dataset?.[attributeToDatasetKey(attribute)];
|
|
27
|
+
if (map && Object.hasOwn(map, rawValue)) return map[rawValue];
|
|
28
|
+
return rawValue;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const sync = () => {
|
|
32
|
+
const value = readValue();
|
|
33
|
+
if (value && typeof value === 'object') {
|
|
34
|
+
simulator.setTheme(value);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
simulator.setThemeMode(typeof value === 'string' && value !== '' ? value : 'dark');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
sync();
|
|
41
|
+
|
|
42
|
+
const Observer = globalThis.MutationObserver;
|
|
43
|
+
if (typeof Observer !== 'function' || typeof source.getAttribute !== 'function') {
|
|
44
|
+
return () => {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const observer = new Observer(sync);
|
|
48
|
+
observer.observe(source, {
|
|
49
|
+
attributes: true,
|
|
50
|
+
attributeFilter: [attribute],
|
|
51
|
+
});
|
|
52
|
+
return () => observer.disconnect();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function attributeToDatasetKey(attribute) {
|
|
56
|
+
return String(attribute)
|
|
57
|
+
.replace(/^data-/, '')
|
|
58
|
+
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
59
|
+
}
|
|
@@ -301,7 +301,8 @@ export class F1SimulatorApp {
|
|
|
301
301
|
this.emitHostCallback('onReady', { snapshot });
|
|
302
302
|
this.resizeHandler = () => {
|
|
303
303
|
this.syncTimingPanelDisclosureState();
|
|
304
|
-
this.
|
|
304
|
+
this.renderSourceSnapshotBuffer ??= {};
|
|
305
|
+
this.applyCamera(this.sim.snapshotRenderInto?.(this.renderSourceSnapshotBuffer) ?? this.sim.snapshotRender?.() ?? this.sim.snapshot());
|
|
305
306
|
};
|
|
306
307
|
window.addEventListener('resize', this.resizeHandler, { signal: this.abortController.signal });
|
|
307
308
|
this.observeLayoutResize();
|
|
@@ -606,7 +607,8 @@ export class F1SimulatorApp {
|
|
|
606
607
|
|
|
607
608
|
syncRendererToCurrentLayout({ render = false } = {}) {
|
|
608
609
|
this.resizeRendererToCanvasHost();
|
|
609
|
-
|
|
610
|
+
this.renderSourceSnapshotBuffer ??= {};
|
|
611
|
+
const snapshot = this.sim?.snapshotRenderInto?.(this.renderSourceSnapshotBuffer) ?? this.sim?.snapshotRender?.() ?? this.sim?.snapshot?.();
|
|
610
612
|
if (!snapshot) return;
|
|
611
613
|
const renderSnapshot = interpolateRenderSnapshotInto(
|
|
612
614
|
this.renderSnapshotBuffer,
|
|
@@ -1191,7 +1193,26 @@ export class F1SimulatorApp {
|
|
|
1191
1193
|
const contextKey = selectedTeamId ?? '';
|
|
1192
1194
|
if (this.lastThemeContextKey === contextKey) return;
|
|
1193
1195
|
this.lastThemeContextKey = contextKey;
|
|
1194
|
-
|
|
1196
|
+
const context = { selectedTeamId };
|
|
1197
|
+
if (typeof this.root?.applyCssVariables === 'function') {
|
|
1198
|
+
this.root.applyCssVariables(context);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
applyPaddockThemeCssVariables(this.root, this.options.theme, context);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
setTheme(theme) {
|
|
1205
|
+
this.options = {
|
|
1206
|
+
...this.options,
|
|
1207
|
+
theme,
|
|
1208
|
+
};
|
|
1209
|
+
this.lastThemeContextKey = null;
|
|
1210
|
+
this.syncThemeContext(this.getSnapshot());
|
|
1211
|
+
return this.options.theme;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
getTheme() {
|
|
1215
|
+
return this.options.theme;
|
|
1195
1216
|
}
|
|
1196
1217
|
|
|
1197
1218
|
getSelectedTeamId(snapshot = null) {
|