@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
package/README.md
CHANGED
|
@@ -1,634 +1,186 @@
|
|
|
1
1
|
# PaddockJS
|
|
2
2
|
|
|
3
|
-
PaddockJS is an installable
|
|
3
|
+
PaddockJS is an installable browser component package for an F1-style project race simulator. It owns the simulator UI, PixiJS runtime, bundled simulator assets, CSS, demo data, and public mount APIs. Host websites provide data, page placement, routing, and optional theme choices.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Code snippets in this README are illustrative host integration fragments unless a section says they are a complete runnable file.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install @inventure71/paddockjs
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Start with [docs/index.md](docs/index.md) for the full package map: what to install, which API to use, what data to pass, how rules modules work, how the browser and headless runtimes differ, and which verification commands prove a change. If you are integrating the package into a host site, read [System Specs](docs/system_specs.md), [Data Contract](docs/data_contract.md), and [Rules](docs/rules.md) first. If you already trained a driver model and want to run it in PaddockJS, use the [Custom Model Controller Guide](docs/custom_model_controller.md).
|
|
16
|
-
|
|
17
|
-
## Package Workflow
|
|
18
|
-
|
|
19
|
-
The repo includes the full package-release boundary:
|
|
20
|
-
|
|
21
|
-
- a tracked showcase host in `local-preview/`
|
|
22
|
-
- public TypeScript declarations in `src/index.d.ts`
|
|
23
|
-
- GitHub Actions CI in `.github/workflows/ci.yml`
|
|
24
|
-
- Changesets release automation in `.github/workflows/release.yml`
|
|
25
|
-
- npm trusted publishing support through GitHub Actions OIDC
|
|
26
|
-
|
|
27
|
-
Useful commands:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
npm run check
|
|
31
|
-
npm run check:release
|
|
32
|
-
npm run consumer:smoke
|
|
33
|
-
npm run browser:smoke
|
|
34
|
-
npm run showcase:dev
|
|
35
|
-
npm run showcase:build
|
|
36
|
-
npm run changeset
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
`npm run check` is the normal local gate: fast runtime tests, public declarations, dry package contents, packed-package consumption in a fresh Vite app, the showcase build, and a quick Chromium smoke against the showcase. `npm run check:release` runs the same package gates plus slow characterization tests and the full browser smoke matrix.
|
|
40
|
-
|
|
41
|
-
Local development and showcase builds require Node `20.19.0` or newer. CI currently runs the package check on Node 22 and releases on Node 24.
|
|
42
|
-
|
|
43
|
-
## 3.0 Release Highlights
|
|
44
|
-
|
|
45
|
-
The `3.0.0` release line adds semantic theme customization with complete resolved light/dark themes, reusable derivative theme packages, component/team theme selectors, cached opposite-mode color generation, and validated color tokens. Indexed track queries are now canonical/internal, so hosts should remove any `trackQueryIndex` option usage. The local preview adds first-class Customization and playable keyboard routes, including theme package switching, selected-team theming, component overrides, pit request/commit/clear, and target-compound controls. The public camera contract includes the opt-in `driver` camera, and full public car snapshots document `car.trackState` as the stable car-center track classification shape.
|
|
46
|
-
|
|
47
|
-
The release also hardens public URL handling for options such as `backLinkHref`, keeps package readouts safe for partial startup or external-renderer snapshots, preserves scheduler cancellation behavior in `createPaddockDriverControllerLoop()`, and keeps the release workflow centered on `npm run check` plus the final `npm run check:release` gate.
|
|
48
|
-
|
|
49
|
-
The package still does not ship trained model weights, model storage, a Python Gymnasium/PettingZoo package, static obstacles, weather, reliability failures, fuel-load effects, or debug mutation APIs. Those remain host-owned or future scope.
|
|
13
|
+
Use Node `20.19.0` or newer for local package development and modern Vite-based host builds.
|
|
50
14
|
|
|
51
|
-
##
|
|
15
|
+
## Browser Mount
|
|
52
16
|
|
|
53
|
-
|
|
17
|
+
For a normal website page, mount the all-in-one simulator from the root browser package:
|
|
54
18
|
|
|
55
19
|
```js
|
|
56
|
-
import {
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
The package root remains the browser component API. The environment subpath is intentionally browser-free and does not import DOM, PixiJS, or package CSS.
|
|
60
|
-
PaddockJS is a bring-your-own-model environment. It does not choose an ML framework, store model weights, or ship a trained driver. The shared `createPaddockDriverControllerLoop()` helper lets a user-owned controller run against either browser expert mode or `createPaddockEnvironment()` with the same batched `decideBatch(context)` call shape.
|
|
61
|
-
|
|
62
|
-
Browser and headless environments default to `physicsMode: 'arcade'` for existing hosts. Opt into the stricter vehicle model with `physicsMode: 'advanced'` when you want 2D velocity/yaw dynamics, traction limits, steering scrub, derived slip telemetry, reduced off-road grip, and advanced-mode AI tuning:
|
|
63
|
-
|
|
64
|
-
```js
|
|
65
|
-
const env = createPaddockEnvironment({
|
|
66
|
-
drivers,
|
|
67
|
-
entries,
|
|
68
|
-
controlledDrivers: ['budget'],
|
|
69
|
-
frameSkip: 2,
|
|
70
|
-
physicsMode: 'advanced',
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
let result = env.reset();
|
|
74
|
-
result = env.step({
|
|
75
|
-
budget: { steering: 0, throttle: 1, brake: 0 },
|
|
76
|
-
});
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
Action steering is an absolute normalized steering target: `-1` points the wheel to maximum left, `0` points it to center, `1` points it to maximum right, and intermediate values target the same percentage of the maximum angle. Physics still rate-limits how quickly the steering wheel reaches that target.
|
|
80
|
-
|
|
81
|
-
The environment can run with no reward, as above, or with a host-supplied `reward(context)` callback. Reward formulas stay user-owned; PaddockJS only supplies the neutral facts:
|
|
82
|
-
|
|
83
|
-
```js
|
|
84
|
-
const env = createPaddockEnvironment({
|
|
85
|
-
drivers,
|
|
86
|
-
entries,
|
|
87
|
-
controlledDrivers: ['budget'],
|
|
88
|
-
reward({ metrics, episode }) {
|
|
89
|
-
if (metrics.destroyed) return -200;
|
|
90
|
-
if (metrics.offTrack) return -12;
|
|
91
|
-
if (episode.terminated) return 0;
|
|
92
|
-
return metrics.legalProgressDeltaMeters;
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Reward callback results are converted to numbers per controlled driver. Missing, `NaN`, or infinite callback results become a neutral `0` so one bad callback value cannot poison a training batch.
|
|
98
|
-
|
|
99
|
-
The repository also includes dependency-free examples that use the same environment contract:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
node examples/train-basic-policy.mjs --generations=4 --candidates=5 --episodes=1 --steps=240
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
The starter script imports the public `@inventure71/paddockjs/environment` subpath and uses self-contained example data from `examples/trainingData.mjs`, so it does not depend on private package source modules for demo drivers. `createProgressReward()` remains available as example/demo reward code only; it is not the official reward and not part of the environment objective.
|
|
106
|
-
|
|
107
|
-
Each default ray reports track-transition distance and car distance. A track hit uses `kind: 'exit'` when the ray leaves the road and `kind: 'entry'` when an off-track ray points back to the road. Richer sensor layouts are opt-in: rays can use per-ray lengths, predefined layouts such as `driver-front-heavy`, channels for `roadEdge`, `kerb`, `illegalSurface`, and `car`, and `precision: 'driver' | 'debug'`. Driver precision is the default model-facing contract; debug precision is only for clearly labeled diagnostics. Surface channels are computed only when requested. Barrier walls are rendered and enforced as hard destruction boundaries in both physics modes, but they are not model-facing ray targets. `observation.object.self.onTrack` follows the simulator's wheel-level legality rules, so track, kerb, and legal pit-lane/box surfaces are on-track for reward and observation purposes. `pitIntent: 0` is always accepted as the no-op clear value, including environments where pit stops are disabled.
|
|
108
|
-
|
|
109
|
-
For realistic local-perception policies, use `physicsMode: 'advanced'` with `observation.profile: 'physical-driver'`. It exposes yaw rate, local boundary distances, contact-patch surface readings, richer opponent radar, and surface-aware ray fields in the versioned vector schema. The profile defaults track lookahead to `[]` so the policy does not receive privileged future curvature unless the host explicitly opts back in.
|
|
110
|
-
|
|
111
|
-
External training code can inspect the environment contract without guessing field ranges:
|
|
20
|
+
import { mountF1Simulator } from '@inventure71/paddockjs';
|
|
112
21
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
22
|
+
const root = document.getElementById('sim-root');
|
|
23
|
+
|
|
24
|
+
if (root) {
|
|
25
|
+
await mountF1Simulator(root, {
|
|
26
|
+
drivers: [
|
|
27
|
+
{
|
|
28
|
+
id: 'budget',
|
|
29
|
+
name: 'Budget Buddy',
|
|
30
|
+
color: '#ff2d55',
|
|
31
|
+
link: '/projects/budget-buddy',
|
|
32
|
+
raceData: ['AI finance coach', 'Python + LLM', 'Budget guardrails'],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
entries: [
|
|
36
|
+
{
|
|
37
|
+
driverId: 'budget',
|
|
38
|
+
driverNumber: 71,
|
|
39
|
+
timingName: 'Budget',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
onDriverOpen(driver) {
|
|
43
|
+
if (driver.link) window.location.href = driver.link;
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
116
47
|
```
|
|
117
48
|
|
|
118
|
-
|
|
49
|
+
The root package is browser-oriented and imports package CSS. If your host build does not extract CSS from JavaScript imports, import the stylesheet explicitly:
|
|
119
50
|
|
|
120
51
|
```js
|
|
121
|
-
import
|
|
122
|
-
|
|
123
|
-
const controller = {
|
|
124
|
-
async decideBatch(ctx) {
|
|
125
|
-
return Object.fromEntries(ctx.orderedObservations.map(({ driverId, vector }) => [
|
|
126
|
-
driverId,
|
|
127
|
-
userModelAction(vector),
|
|
128
|
-
]));
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const loop = createPaddockDriverControllerLoop({
|
|
133
|
-
runtime: env,
|
|
134
|
-
controller,
|
|
135
|
-
actionRepeat: 4,
|
|
136
|
-
});
|
|
137
|
-
await loop.reset();
|
|
138
|
-
await loop.step();
|
|
52
|
+
import '@inventure71/paddockjs/styles.css';
|
|
139
53
|
```
|
|
140
54
|
|
|
141
|
-
|
|
55
|
+
PaddockJS CSS is scoped under package mount/component roots and uses system font stacks. The package does not load Google Fonts or any other remote font. Hosts that want branded typography should load fonts in the host app.
|
|
142
56
|
|
|
143
|
-
|
|
57
|
+
## Startup Loading
|
|
144
58
|
|
|
145
|
-
|
|
146
|
-
const env = createPaddockEnvironment({
|
|
147
|
-
drivers,
|
|
148
|
-
entries,
|
|
149
|
-
controlledDrivers: ['budget'],
|
|
150
|
-
scenario: {
|
|
151
|
-
preset: 'off-track-recovery',
|
|
152
|
-
placements: {
|
|
153
|
-
budget: { distanceMeters: 420, offsetMeters: 16, speedKph: 65 },
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
```
|
|
59
|
+
`mountF1Simulator(root, options)` writes a lightweight package-owned HTML shell into `root` before it awaits PixiJS setup, asset loading, control binding, simulation creation, and the first canvas frame. That shell includes package loading overlays for the race controls, timing tower, race canvas, race-data panel, telemetry stack, and any other startup-capable package surface in the selected template. The overlays are removed only after the runtime has initialized and the initial readouts/frame are ready.
|
|
158
60
|
|
|
159
|
-
|
|
61
|
+
Composable hosts get the same behavior per mounted root: every public `simulator.mount*()` surface renders static package markup plus its own loading overlay before `await simulator.start()`.
|
|
160
62
|
|
|
161
|
-
|
|
63
|
+
This is a client-side startup shell, not server rendering. If a host wants visible content before the PaddockJS JavaScript bundle has downloaded and executed, use the tiny placeholder subpath and CSS:
|
|
162
64
|
|
|
163
65
|
```js
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
entries,
|
|
167
|
-
controlledDrivers: agentIds,
|
|
168
|
-
physicsMode: 'advanced',
|
|
169
|
-
participantInteractions: { defaultProfile: 'batch-training' },
|
|
170
|
-
observation: {
|
|
171
|
-
profile: 'physical-driver',
|
|
172
|
-
output: 'vector',
|
|
173
|
-
includeSchema: false,
|
|
174
|
-
vectorType: 'float32',
|
|
175
|
-
},
|
|
176
|
-
result: {
|
|
177
|
-
stateOutput: 'none',
|
|
178
|
-
resetDriversObservationScope: 'reset',
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const schema = env.getObservationSpec();
|
|
183
|
-
let result = env.reset();
|
|
184
|
-
result = env.step(actionsByDriver);
|
|
66
|
+
import { createPaddockLoadingPlaceholder } from '@inventure71/paddockjs/placeholder';
|
|
67
|
+
import '@inventure71/paddockjs/placeholder.css';
|
|
185
68
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}, {
|
|
189
|
-
stateOutput: 'none',
|
|
190
|
-
observationScope: 'reset',
|
|
69
|
+
root.innerHTML = createPaddockLoadingPlaceholder({
|
|
70
|
+
label: 'Loading simulator',
|
|
191
71
|
});
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
`batch-training` cars remain real rendered cars in `snapshot.cars`, but they are non-colliding, sensor-hidden, pit-non-blocking, and excluded from race order by default. Step results include `info.drivers[driverId]` episode state and neutral `metrics[driverId]` facts for external logging or user-defined rewards. `stateOutput: 'none'` suppresses repeated `state.snapshot` payloads for high-throughput loops; use `minimal` when the loop still needs the observation snapshot, or omit the option for the full backward-compatible public snapshot. Deterministic evaluation helpers can accept compact no-state base options and internally request the minimal snapshot needed for evaluation metrics. Reset placements are classified against the same runoff/barrier rules before observations are returned: recovery starts stay physical, while cars placed inside terminal barrier space return destroyed metrics and stable miss-valued rays instead of running far-out ray geometry.
|
|
195
|
-
|
|
196
|
-
On the package's local 20-car simulator benchmark with front-heavy physical-driver rays, compact vector/no-state output measured around `3.3ms` per environment action, with a no-ray baseline around `1.5ms`. Those numbers are hardware dependent, but they show the intended usage: keep schema/spec lookup separate, request compact vectors in training loops, and reserve full snapshots for debugging or visualization.
|
|
197
|
-
|
|
198
|
-
For multi-car training and visual comparison, PaddockJS separates real participants from replay overlays:
|
|
199
|
-
|
|
200
|
-
- `participantInteractions` changes how physics-driven cars interact with collisions, sensors, pit occupancy, and race order. Those cars remain in `snapshot.cars` and still move through steering, throttle, brake, pit intent, tire state, timing, and rules.
|
|
201
|
-
- `replayGhosts` are trajectory-driven overlays for reference laps, debugging, or comparison. They appear in `snapshot.replayGhosts`, never in `snapshot.cars`, and do not collide, rank, pit, or trigger penalties. They are sensor-hidden by default and only appear in rays or nearby observations when their own sensor flags opt in.
|
|
202
|
-
- In the browser view, non-colliding participants remain solid clickable cars but get a blue no-collision outline marker. Replay ghosts remain translucent overlays.
|
|
203
|
-
|
|
204
|
-
```js
|
|
205
|
-
const env = createPaddockEnvironment({
|
|
206
|
-
drivers,
|
|
207
|
-
entries,
|
|
208
|
-
controlledDrivers: ['model-a', 'model-b'],
|
|
209
|
-
participantInteractions: {
|
|
210
|
-
drivers: {
|
|
211
|
-
'model-a': { profile: 'isolated-training' },
|
|
212
|
-
'model-b': { profile: 'isolated-training' },
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
```
|
|
217
72
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Set `expert.visualizeSensors` to draw expert sensor rays inside the actual race canvas for visual debugging:
|
|
73
|
+
const loadSimulator = async () => {
|
|
74
|
+
const { mountF1Simulator } = await import('@inventure71/paddockjs');
|
|
75
|
+
await mountF1Simulator(root, { drivers, entries });
|
|
76
|
+
};
|
|
223
77
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
enabled: true,
|
|
230
|
-
controlledDrivers: ['budget'],
|
|
231
|
-
frameSkip: 4,
|
|
232
|
-
visualizeSensors: {
|
|
233
|
-
rays: true,
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
});
|
|
78
|
+
if ('requestIdleCallback' in window) {
|
|
79
|
+
window.requestIdleCallback(loadSimulator);
|
|
80
|
+
} else {
|
|
81
|
+
setTimeout(loadSimulator, 0);
|
|
82
|
+
}
|
|
237
83
|
```
|
|
238
84
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
When multiple drivers are controlled, sensor visualization renders the selected controlled driver by default so batch-training previews do not draw every agent's rays every frame. Use `visualizeSensors: { rays: true, drivers: 'all' }` only when you intentionally want the heavier all-controlled-car overlay.
|
|
242
|
-
|
|
243
|
-
## API
|
|
85
|
+
`@inventure71/paddockjs/placeholder` does not import PixiJS, simulator assets, package runtime CSS, or the root browser mount. It is intended for the pre-JS/network gap. The full simulator mount replaces that placeholder and then uses the built-in package overlays while PixiJS and the race runtime finish booting.
|
|
244
86
|
|
|
245
|
-
|
|
87
|
+
Start here:
|
|
246
88
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
import { mountF1Simulator } from '@inventure71/paddockjs';
|
|
89
|
+
- [Getting Started](docs/getting-started.md): install, browser mount, CSS, assets, host responsibilities, and container sizing.
|
|
90
|
+
- [Data Contract](docs/data_contract.md): driver, entry, callback, rules, and option shapes.
|
|
91
|
+
- [Troubleshooting](docs/troubleshooting.md): common integration failures and supported checks.
|
|
251
92
|
|
|
252
|
-
|
|
253
|
-
preset: 'timing-overlay',
|
|
254
|
-
drivers: [
|
|
255
|
-
{
|
|
256
|
-
id: 'budget',
|
|
257
|
-
name: 'Budget Buddy',
|
|
258
|
-
color: '#ff2d55',
|
|
259
|
-
link: '/project_details/project-budget-buddy.html',
|
|
260
|
-
raceData: ['AI finance coach', 'Python + LLM', 'Budget guardrails'],
|
|
261
|
-
},
|
|
262
|
-
],
|
|
263
|
-
entries: [
|
|
264
|
-
{
|
|
265
|
-
driverId: 'budget',
|
|
266
|
-
driverNumber: 71,
|
|
267
|
-
timingName: 'Budget',
|
|
268
|
-
driver: { pace: 52, racecraft: 74, aggression: 38, riskTolerance: 47, patience: 81, consistency: 86 },
|
|
269
|
-
vehicle: { id: 'budget-bb01', name: 'BB-01 Ledger', power: 48, braking: 72, aero: 55, dragEfficiency: 66, mechanicalGrip: 63, weightControl: 58, tireCare: 82 },
|
|
270
|
-
},
|
|
271
|
-
],
|
|
272
|
-
initialCameraMode: 'show-all',
|
|
273
|
-
expert: {
|
|
274
|
-
enabled: true,
|
|
275
|
-
controlledDrivers: ['budget'],
|
|
276
|
-
},
|
|
277
|
-
onDriverOpen(driver) {
|
|
278
|
-
window.location.href = driver.link;
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
```
|
|
93
|
+
## Composable Layouts
|
|
282
94
|
|
|
283
|
-
|
|
95
|
+
Use `createPaddockSimulator()` when a host wants to place package-owned surfaces in separate roots while keeping one simulator controller:
|
|
284
96
|
|
|
285
97
|
```js
|
|
286
98
|
import { createPaddockSimulator } from '@inventure71/paddockjs';
|
|
287
99
|
|
|
288
|
-
const simulator = createPaddockSimulator({
|
|
289
|
-
drivers,
|
|
290
|
-
entries,
|
|
291
|
-
onDriverOpen(driver) {
|
|
292
|
-
window.location.href = driver.link;
|
|
293
|
-
},
|
|
294
|
-
});
|
|
100
|
+
const simulator = createPaddockSimulator({ drivers, entries });
|
|
295
101
|
|
|
296
102
|
simulator.mountRaceControls(document.getElementById('sim-controls'));
|
|
297
|
-
simulator.mountCameraControls(document.getElementById('sim-camera-controls'));
|
|
298
|
-
simulator.mountSafetyCarControl(document.getElementById('sim-safety-car'));
|
|
299
|
-
simulator.mountTimingTower(document.getElementById('sim-timing'));
|
|
300
103
|
simulator.mountRaceCanvas(document.getElementById('sim-race'), {
|
|
104
|
+
includeTimingTower: true,
|
|
301
105
|
includeRaceDataPanel: true,
|
|
302
106
|
});
|
|
303
|
-
simulator.
|
|
304
|
-
simulator.mountTelemetrySectors(document.getElementById('sim-telemetry-sectors'));
|
|
305
|
-
simulator.mountTelemetrySectorBanner(document.getElementById('sim-telemetry-sector-banner'));
|
|
306
|
-
simulator.mountTelemetryLapTimes(document.getElementById('sim-telemetry-laps'));
|
|
307
|
-
simulator.mountTelemetrySectorTimes(document.getElementById('sim-telemetry-sector-times'));
|
|
308
|
-
simulator.mountCarDriverOverview(document.getElementById('sim-overview'));
|
|
107
|
+
simulator.mountRaceDataPanel(document.getElementById('sim-race-data'));
|
|
309
108
|
|
|
310
109
|
await simulator.start();
|
|
311
110
|
```
|
|
312
111
|
|
|
313
|
-
The
|
|
112
|
+
The controller methods are the canonical composable API. See [Composable Layouts](docs/composable-layouts.md) for the available surfaces, lifecycle, restart behavior, and sizing contract.
|
|
314
113
|
|
|
315
|
-
|
|
316
|
-
simulator.mountRaceCanvas(document.getElementById('sim-race'), {
|
|
317
|
-
includeTimingTower: true,
|
|
318
|
-
includeRaceDataPanel: true,
|
|
319
|
-
timingTowerVerticalFit: 'scroll',
|
|
320
|
-
});
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
For a packaged race-window template with a right-side telemetry drawer:
|
|
114
|
+
## Headless And Data Subpaths
|
|
324
115
|
|
|
325
|
-
|
|
326
|
-
simulator.mountRaceTelemetryDrawer(document.getElementById('sim-race-workbench'), {
|
|
327
|
-
timingTowerVerticalFit: 'expand-race-view',
|
|
328
|
-
raceDataTelemetryDetail: true,
|
|
329
|
-
});
|
|
330
|
-
```
|
|
116
|
+
Do not import the root package from raw Node or headless training code. The root package is for browser mounts and imports CSS/assets.
|
|
331
117
|
|
|
332
|
-
|
|
118
|
+
Use the browser-free environment subpath for simulation/training loops:
|
|
333
119
|
|
|
334
120
|
```js
|
|
335
|
-
|
|
336
|
-
drivers,
|
|
337
|
-
entries,
|
|
338
|
-
rules: {
|
|
339
|
-
ruleset: 'custom',
|
|
340
|
-
modules: {
|
|
341
|
-
pitStops: { enabled: true },
|
|
342
|
-
stalledDnf: { enabled: true },
|
|
343
|
-
tireStrategy: { enabled: true, mandatoryDistinctDryCompounds: 2 },
|
|
344
|
-
},
|
|
345
|
-
},
|
|
346
|
-
});
|
|
121
|
+
import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
|
|
347
122
|
```
|
|
348
123
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
The returned object supports:
|
|
352
|
-
|
|
353
|
-
- `destroy()`
|
|
354
|
-
- `restart(nextOptions)` for non-asset, non-expert race/data/seed changes
|
|
355
|
-
- `selectDriver(driverId)`
|
|
356
|
-
- `setSafetyCarDeployed(deployed)`
|
|
357
|
-
- `setRedFlagDeployed(deployed)`
|
|
358
|
-
- `setPitLaneOpen(open)`
|
|
359
|
-
- `callSafetyCar()`
|
|
360
|
-
- `clearSafetyCar()`
|
|
361
|
-
- `toggleSafetyCar()`
|
|
362
|
-
- `setPitIntent(driverId, intent, targetCompound?)`
|
|
363
|
-
- `getPitIntent(driverId)`
|
|
364
|
-
- `getPitTargetCompound(driverId)`
|
|
365
|
-
- `getSimulationSpeed()`
|
|
366
|
-
- `setTimingGapMode(mode)`
|
|
367
|
-
- `getTimingGapMode()`
|
|
368
|
-
- `toggleTimingGapMode()`
|
|
369
|
-
- `servePenalty(penaltyId)`
|
|
370
|
-
- `cancelPenalty(penaltyId)`
|
|
371
|
-
- `getSnapshot()`
|
|
372
|
-
- `expert` when explicitly enabled, otherwise `null`
|
|
373
|
-
|
|
374
|
-
Timing gap mode is controller-owned, not tied to whether the manual tower toggle is visible. `setTimingGapMode('interval')` makes timing rows show interval to the car ahead, `setTimingGapMode('leader')` makes them show gap to P1, and `toggleTimingGapMode()` switches between those two values. These methods can be called after mounting or while the race is running; mounted timing towers update immediately and keep the same selected driver/order state.
|
|
375
|
-
|
|
376
|
-
Useful UI options:
|
|
124
|
+
Use the CSS-free data subpath for tooling, server-side data normalization, and procedural track helpers:
|
|
377
125
|
|
|
378
126
|
```js
|
|
379
|
-
|
|
380
|
-
theme: {
|
|
381
|
-
mode: 'system',
|
|
382
|
-
use: 'trackside',
|
|
383
|
-
tokens: {
|
|
384
|
-
primary: { light: '#008c55', dark: '#00ff84' },
|
|
385
|
-
pitLane: '#7c3aed',
|
|
386
|
-
},
|
|
387
|
-
themes: {
|
|
388
|
-
trackside: {
|
|
389
|
-
extends: 'default',
|
|
390
|
-
tokens: { yellowFlag: { dark: '#ffcc00' } },
|
|
391
|
-
components: {
|
|
392
|
-
button: { background: 'pitLane', text: 'primaryText', border: 'primary' },
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
componentThemes: {
|
|
397
|
-
'race-controls': 'trackside',
|
|
398
|
-
},
|
|
399
|
-
timingTowerMaxWidth: '360px',
|
|
400
|
-
raceViewMinHeight: '720px',
|
|
401
|
-
},
|
|
402
|
-
ui: {
|
|
403
|
-
layoutPreset: 'left-tower-overlay',
|
|
404
|
-
cameraControls: 'external',
|
|
405
|
-
showFps: false,
|
|
406
|
-
telemetryIncludesOverview: false,
|
|
407
|
-
telemetryModules: ['core', 'sectors', 'lapTimes', 'sectorTimes'],
|
|
408
|
-
raceDataBanners: {
|
|
409
|
-
initial: 'project',
|
|
410
|
-
enabled: ['project', 'radio'],
|
|
411
|
-
},
|
|
412
|
-
raceDataBannerSize: 'auto',
|
|
413
|
-
raceDataTelemetryDetail: true,
|
|
414
|
-
timingTowerVerticalFit: 'expand-race-view',
|
|
415
|
-
timingGapMode: 'interval',
|
|
416
|
-
timingGapModeToggle: true,
|
|
417
|
-
timingEntryVerticalPadding: 5,
|
|
418
|
-
},
|
|
419
|
-
debug: {
|
|
420
|
-
physicsModeIndicator: false,
|
|
421
|
-
}
|
|
127
|
+
import { DriverData, normalizeSimulatorDrivers } from '@inventure71/paddockjs/data';
|
|
422
128
|
```
|
|
423
129
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
### Layout Support Contract
|
|
427
|
-
|
|
428
|
-
PaddockJS supports the all-in-one shell and host mount roots in containers at least `320px` wide. Hosts should size the outer mount root with normal responsive CSS, for example `width: 100%; min-width: 0;`, and avoid adding host rules that target package internals such as `.sim-grid`, `.sim-timing`, or `.race-data-panel`. The host owns page placement; PaddockJS owns the shell, timing-board width, camera safe area, lower-thirds, drawer behavior, focus styles, and touch target sizing. Package-owned subcomponents can have smaller internal widths when their own layout is designed for it; for example, the broadcast timing tower remains valid at its package-owned narrow width. Responsive narrow template variants are enabled by default and can be disabled with `ui.responsiveNarrowLayout: false` or the matching composable mount option when a host intentionally wants the older stacked timing/drawer behavior.
|
|
429
|
-
|
|
430
|
-
The package uses container-width density tiers:
|
|
130
|
+
Read [Headless Environment](docs/headless-environment.md), [Data Helpers](docs/data-helpers.md), and [Bring Your Own Model](docs/training.md) for those paths.
|
|
431
131
|
|
|
432
|
-
|
|
433
|
-
- `520px` to `759px`: narrow tablet density. Race surfaces keep readable controls while standalone telemetry and overview modules collapse to single-column layouts.
|
|
434
|
-
- `760px` to `1119px`: tablet density. Side-by-side host sections may still give an individual component a narrow container, so package components use their own container width rather than viewport width.
|
|
435
|
-
- `1120px` and wider: desktop density. The broadcast timing tower can sit in the left race gutter at its package-owned width, and telemetry/drawer layouts use wider horizontal space.
|
|
132
|
+
## Runtime Theming
|
|
436
133
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
Package controls, drawer toggles, lower-third actions, and close buttons maintain practical `44px` touch targets. Timing rows are compact selectable broadcast entries with configurable vertical padding. Touch driving controls are not part of the package; the playable preview remains keyboard/expert-action driven and does not add a virtual joystick.
|
|
440
|
-
|
|
441
|
-
If `trackSeed` is omitted, each mounted browser simulator creates a fresh procedural circuit. Passing `trackSeed` makes the track deterministic so multiple embeds can share the same generated circuit; repeated procedural seeds are cached within the page runtime as immutable track definitions. Treat values returned by `createProceduralTrack()` as read-only and pass custom mutable copies when experimenting with track-definition edits. `restart({ trackSeed })` rebuilds the race on the deterministic circuit for the new seed. Asset URL changes are not restartable; destroy and mount a new simulator when changing assets.
|
|
442
|
-
|
|
443
|
-
Warmup is enabled by default across browser, headless environment, and direct simulation creation. The runtime warms a disposable instance during loading and caches by configuration fingerprint, so identical resets/restarts skip repeated warmup while seed/config changes warm again automatically. Override with `warmup: { enabled, policy: 'config-change' | 'always' | 'never', steps }` or `warmup: false`.
|
|
444
|
-
|
|
445
|
-
Generated circuits are built from seeded connected region boundaries that are smoothed and warped into a validated centerline, so tracks can include concave infield/outfield sections and chicane-like bends instead of simple oval-like fallback shapes. Hosts can pass `trackGeneration` alongside `trackSeed` to choose a profile and override semantic generation controls:
|
|
134
|
+
Use mount-time `theme` for defaults and controller methods for runtime site theme toggles:
|
|
446
135
|
|
|
447
136
|
```js
|
|
448
|
-
const simulator = await mountF1Simulator(root, {
|
|
449
|
-
drivers,
|
|
450
|
-
entries,
|
|
451
|
-
trackSeed: 4101,
|
|
452
|
-
trackGeneration: {
|
|
453
|
-
profile: 'training-short',
|
|
454
|
-
length: { minMeters: 900, maxMeters: 1800 },
|
|
455
|
-
startStraight: { gridMeters: 0 },
|
|
456
|
-
pitLane: { enabled: false },
|
|
457
|
-
},
|
|
458
|
-
rules: {
|
|
459
|
-
modules: {
|
|
460
|
-
pitStops: { enabled: false },
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
});
|
|
137
|
+
const simulator = await mountF1Simulator(root, { drivers, entries });
|
|
464
138
|
|
|
465
|
-
simulator.
|
|
466
|
-
trackSeed: 5051,
|
|
467
|
-
trackGeneration: { profile: 'race' },
|
|
468
|
-
});
|
|
469
|
-
```
|
|
139
|
+
simulator.setThemeMode(document.documentElement.dataset.theme === 'light' ? 'light' : 'dark');
|
|
470
140
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
import { createProceduralTrack } from '@inventure71/paddockjs';
|
|
475
|
-
|
|
476
|
-
const trainingTrack = createProceduralTrack(4101, {
|
|
477
|
-
profile: 'training-short',
|
|
478
|
-
length: { minMeters: 900, maxMeters: 1800 },
|
|
479
|
-
startStraight: { gridMeters: 0, exitMeters: 80, blendMeters: 80 },
|
|
480
|
-
pitLane: { enabled: false },
|
|
481
|
-
shape: { scale: 0.2, cornerDensity: 1.3, variation: 0.22 },
|
|
482
|
-
validation: { minClearanceMultiplier: 1, maxLocalTurnRadians: 1.85 },
|
|
483
|
-
attempts: { primary: 80, fallback: 200 },
|
|
141
|
+
const stopThemeSync = simulator.syncThemeFrom(document.documentElement, {
|
|
142
|
+
attribute: 'data-theme',
|
|
143
|
+
map: { light: 'light', dark: 'dark' },
|
|
484
144
|
});
|
|
485
145
|
```
|
|
486
146
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
Pit-lane geometry depends on the resolved generation options. The `race` profile includes:
|
|
490
|
-
|
|
491
|
-
- rendered pit lane beside the start/finish straight
|
|
492
|
-
- lane-aligned procedural entry and exit roads
|
|
493
|
-
- main fast lane sized from the team/box layout
|
|
494
|
-
- parallel working lane
|
|
495
|
-
- 10 shared team service areas
|
|
496
|
-
- 20 unused garage boxes arranged as 10 team pairs
|
|
497
|
-
|
|
498
|
-
Training profiles disable pit-lane generation by default. If a host explicitly sets `pitLane: { enabled: false }`, pit-related rules should also be disabled or left in a no-op configuration.
|
|
499
|
-
|
|
500
|
-
Pit-lane asphalt, working-lane service areas, and garage boxes are legal drivable surfaces for sensors, runoff handling, track-limit stewarding, and stalled-DNF timing. In both physics modes, the rendered barrier wall marks the hard outer runoff boundary; cars whose footprint reaches the wall's inner face are marked `destroyed`, stopped, removed from active collision/sensor participation, and treated as DNF entries at the bottom of the timing order. `physicsMode` changes vehicle integration and grip behavior, not barrier consequences. Environment results expose destruction as neutral `metrics[driverId].destroyed` and a per-driver `endReason: 'destroyed'`, so training loops can assign their own negative reward and then call `resetDrivers()` for a new episode. Opt-in stalled off-track DNF uses `endReason: 'stalled-off-track'` without setting `destroyed`. Tire energy degrades down to 1% and affects grip nonlinearly, so badly worn tires are slower and harder to rotate without making the car instantly undrivable.
|
|
501
|
-
|
|
502
|
-
When `rules.modules.pitStops.enabled` is true, cars automatically form bounded pit trains when lane space is available. They brake to the limiter by the main pit-lane start, drive along the main fast lane, pass through the team queue spot as a rolling gate, roll into the team-colored working-lane service area when it is clear, stop, serve eligible penalties before tire work, show the remaining stationary service time above the car, change to the requested configured tire compound or the default alternate compound, and exit back to the race track.
|
|
147
|
+
Do not restart the simulator only to change presentation. Runtime theme APIs keep race state, selected driver, penalties, speed, camera state, and pit intent state. See [Theming](docs/theming.md).
|
|
503
148
|
|
|
504
|
-
|
|
149
|
+
## Upgrade From 2.x
|
|
505
150
|
|
|
506
|
-
|
|
151
|
+
If a host is moving from 2.x-era usage, use [Upgrading Hosts To PaddockJS 4.x](docs/upgrading-hosts-to-3.md). The short version:
|
|
507
152
|
|
|
508
|
-
|
|
153
|
+
- install `@inventure71/paddockjs@4`
|
|
154
|
+
- remove `trackQueryIndex`
|
|
155
|
+
- replace `physicsMode: 'simulator'` with `physicsMode: 'advanced'` or omit it for `arcade`
|
|
156
|
+
- import package CSS intentionally for browser mounts
|
|
157
|
+
- use `/environment` for headless simulation and `/data` for CSS-free helpers
|
|
158
|
+
- rebuild and browser-smoke the host page
|
|
509
159
|
|
|
510
|
-
|
|
160
|
+
## Reference Docs
|
|
511
161
|
|
|
512
|
-
|
|
513
|
-
- `1` means keep trying until a free pit-entry window is available
|
|
514
|
-
- `2` means commit to entering at the next pit-entry window even when pit-lane capacity or gap checks would block an opportunistic stop
|
|
162
|
+
Normal consumers should start with the task docs above. These reference docs are the deeper package source of truth:
|
|
515
163
|
|
|
516
|
-
|
|
164
|
+
- [System Specs](docs/system_specs.md): public API, runtime guarantees, and verification standards.
|
|
165
|
+
- [Rules](docs/rules.md): race-control behavior, DRS, safety car, starts, ordering, contact handling, pits, penalties, and simulation rules.
|
|
166
|
+
- [Concepts](docs/concepts.md): simulator vocabulary.
|
|
167
|
+
- [Architecture](docs/architecture.md): module ownership and data/control flow for package contributors.
|
|
168
|
+
- [Component Inventory](docs/component_inventory.md): package-owned UI surfaces and template ownership.
|
|
517
169
|
|
|
518
|
-
|
|
170
|
+
## Package Workflow
|
|
519
171
|
|
|
520
|
-
|
|
172
|
+
Useful local commands:
|
|
521
173
|
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
enabled: true,
|
|
530
|
-
pitLaneSpeedLimitKph: 80,
|
|
531
|
-
maxConcurrentPitLaneCars: 3,
|
|
532
|
-
minimumPitLaneGapMeters: 20,
|
|
533
|
-
tirePitRequestThresholdPercent: 50,
|
|
534
|
-
tirePitCommitThresholdPercent: 30,
|
|
535
|
-
variability: {
|
|
536
|
-
enabled: true,
|
|
537
|
-
perfect: false,
|
|
538
|
-
},
|
|
539
|
-
},
|
|
540
|
-
tireStrategy: {
|
|
541
|
-
enabled: true,
|
|
542
|
-
mandatoryDistinctDryCompounds: 2,
|
|
543
|
-
},
|
|
544
|
-
tireDegradation: {
|
|
545
|
-
enabled: true,
|
|
546
|
-
},
|
|
547
|
-
stalledDnf: {
|
|
548
|
-
enabled: true,
|
|
549
|
-
maxStoppedSeconds: 12,
|
|
550
|
-
speedThresholdKph: 5,
|
|
551
|
-
},
|
|
552
|
-
penalties: {
|
|
553
|
-
trackLimits: { strictness: 0.8 },
|
|
554
|
-
collision: { strictness: 0.5, consequences: [{ type: 'time', seconds: 5 }] },
|
|
555
|
-
tireRequirement: { strictness: 1, consequences: [{ type: 'time', seconds: 10 }] },
|
|
556
|
-
pitLaneSpeeding: { strictness: 1, speedLimitKph: 80 },
|
|
557
|
-
},
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
ui: {
|
|
561
|
-
penaltyBanners: true,
|
|
562
|
-
timingPenaltyBadges: true,
|
|
563
|
-
},
|
|
564
|
-
});
|
|
174
|
+
```bash
|
|
175
|
+
npm run docs:check
|
|
176
|
+
npm run check
|
|
177
|
+
npm run check:release
|
|
178
|
+
npm run consumer:smoke
|
|
179
|
+
npm run browser:smoke:quick -- --skip-build
|
|
180
|
+
npm run showcase:dev
|
|
565
181
|
```
|
|
566
182
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
Supported rulesets are `paddock`, `grandPrix2025`, `fia2025`, and `custom`. Presets only choose defaults; explicit module config wins. Weather, reliability, and fuel-load performance effects are reserved future modules and are not active `3.0.0` behavior. Penalty subsections use `strictness` from `0` to `1` instead of plain booleans. Track limits use the white line as the legal edge and require all four wheel contact patches to be fully outside the same side of the line before recording a violation, so normal kerb riding is not punished. Per-car `surface` is resolved from the worst wheel surface, snapshots include `car.wheels` for per-wheel surface and white-line state, and asymmetric left/right wheel resistance adds a small capped yaw tug toward the slower side when only one side is on a worse surface. Collision stewarding is driven by a body collision hull, not transparent sprite bounds or wheel-only overlap, and contact events include shape ids, contact type, depth, and time of impact. It considers impact severity, closing speed, and whether one car clearly hit another from behind; clear rear contact penalizes only the physically trailing car, including lapped traffic cases, while unclear meaningful contact records shared-fault penalties for both cars. Stalled off-track DNFs emit `car-dnf` with `reason: 'stalled-off-track'`, freeze the car, remove it from active control/collision/sensor/pit participation, and classify it with the existing DNF metadata. Pit-lane speeding is enforced on the main fast lane, working lane, service areas, and garage boxes, but not on pit-entry or pit-exit connector roads. Track-limit warnings are emitted as `track-limits` events, while penalty decisions are exposed through `snapshot.penalties` plus `penalty` events. Penalty consequences support warning, time, drive-through, stop-go, position-drop, grid-drop, and disqualification payloads. Time consequences are additive, drive-through and stop-go penalties are service obligations, and unserved service penalties convert to configured time at final classification.
|
|
570
|
-
|
|
571
|
-
`initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, `'driver'`, `'show-all'`, or `'pit'`; invalid values fall back to `'leader'`. The overview camera frames the active generated track bounds, including package-owned track padding and pit-lane extent, instead of using a fixed world-center zoom. Camera mode changes ease from the current camera target to the next target after the initial frame, so switching between leader, selected, driver, overview, show-all, and pit views does not snap the world view.
|
|
572
|
-
|
|
573
|
-
The pit camera targets the generated operational pit-lane geometry, including the lane, boxes, service areas, and queue areas, while excluding the longer entry/exit access roads. It zooms out as needed to fit that work area inside the active race-view safe area, and its control is hidden when the active track has no pit lane.
|
|
574
|
-
|
|
575
|
-
The driver camera follows the selected car from a lower screen anchor and rotates the world so the car's heading points upward. Its control is opt-in through `ui.driverCamera: true`, and setting `initialCameraMode: 'driver'` enables that control automatically.
|
|
576
|
-
|
|
577
|
-
Zoom buttons and wheel zoom apply to every camera mode. Zoom-out is bounded by the active generated track frame so the camera cannot pull far beyond the circuit. The renderer keeps grass coverage larger than the simulated world, and the transparent canvas host uses the same grass color as a fallback around the framed circuit.
|
|
578
|
-
|
|
579
|
-
`layoutPreset: 'left-tower-overlay'` creates a left broadcast gutter inside the race view, frames the camera around the remaining race area, and places the timing tower in that gutter without covering camera controls. The project/radio lower-third stays inside the race window and can intentionally render over the timing sidebar instead of shrinking around it.
|
|
580
|
-
|
|
581
|
-
Composable hosts should pass `{ includeRaceDataPanel: true }` to `mountRaceCanvas()` when they want that lower-third clipped and layered by the race window. `mountRaceDataPanel()` remains available for hosts that intentionally want the banner as a standalone surface.
|
|
582
|
-
|
|
583
|
-
`raceDataTelemetryDetail: true` makes the project lower-third include a compact S1/S2/S3 sector strip with live sector elapsed time and per-sector progress. That telemetry project lower-third stays visible until dismissed, muted, or replaced, while radio mode keeps its normal schedule. The standalone `mountTelemetrySectorBanner()` surface remains available only when a host explicitly mounts it.
|
|
584
|
-
|
|
585
|
-
`includeTimingTower: true` embeds the timing tower directly inside the race canvas and reserves camera space from the measured tower gutter when the tower is a side overlay. In narrow hosts, the embedded timing tower becomes a package-owned left reveal panel opened by the race view's Timing control instead of taking permanent height above the track. The control updates `aria-expanded`, the closed tower is inert/hidden to assistive tech, and the renderer/camera keep resizing through the transition without remounting the simulator. This responsive narrow behavior is enabled by default; pass `responsiveNarrowLayout: false` to `mountRaceCanvas()` or set `ui.responsiveNarrowLayout: false` to opt out.
|
|
586
|
-
|
|
587
|
-
`mountRaceTelemetryDrawer()` is a higher-level template that mounts an external top control row, race canvas, embedded timing tower, lower-third, top steward message, safety-car control, and a telemetry drawer together. Pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact sector detail. On narrow hosts the responsive template is enabled by default: the top workbench controls stack into full-width rows, the safety-car and telemetry actions become a two-column row, the telemetry drawer reveals over the race view instead of forcing the race canvas to reserve permanent drawer height, and the race view measures lower-third/timing-entry geometry before adding extra vertical clearance. Pass `{ responsiveNarrowLayout: false }` to opt out.
|
|
588
|
-
|
|
589
|
-
The drawer top row contains camera controls, a `1x` simulation-speed button that cycles through `2x`, `3x`, `4x`, `5x`, `10x`, and back to `1x`, a `Mute banners` toggle, safety car, and the telemetry toggle so those controls do not cover the race. For other camera-control placements the speed button is hidden by default and can be enabled with `ui.simulationSpeedControl: true`.
|
|
590
|
-
|
|
591
|
-
`Mute banners` is off by default and temporarily suppresses project/radio lower-thirds while it is pressed; steward penalty banners are separate and remain controlled by `penaltyBanners`. Opening the drawer reduces the race area instead of overlaying it, and closing it removes the drawer from interaction. The drawer uses the same package-owned `telemetry-stack` component as `mountTelemetryPanel()`, so constrained host heights scroll telemetry vertically instead of letting the sidebar escape its frame.
|
|
592
|
-
|
|
593
|
-
Banner and timing layout options:
|
|
594
|
-
|
|
595
|
-
- `raceDataBanners.initial` selects the starting banner state: `'project'`, `'radio'`, or `'hidden'`.
|
|
596
|
-
- `raceDataBanners.enabled` chooses which banner types may appear.
|
|
597
|
-
- `raceDataBannerSize: 'auto'` uses the race space to the right of the timing board when it is wide enough and falls back to the full lower-third overlap when it is not.
|
|
598
|
-
- `raceDataBannerSize: 'custom'` preserves the default CSS-variable-driven lower-third size for hosts that want to tune their own banner geometry.
|
|
599
|
-
- `timingTowerVerticalFit: 'expand-race-view'` lets the race window grow tall enough for the tower.
|
|
600
|
-
- `timingTowerVerticalFit: 'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower.
|
|
601
|
-
- `timingGapMode: 'interval'` starts the timing tower in `Int` mode, showing interval to the car ahead. Use `'leader'` to start in `Gap` mode, showing the total gap to P1.
|
|
602
|
-
- `timingGapModeToggle: true` shows the compact timing-tower header toggle by default. Set it to `false` when the host should own mode changes through `setTimingGapMode()`, `getTimingGapMode()`, or `toggleTimingGapMode()`.
|
|
603
|
-
- `timingEntryVerticalPadding: 5` adds `5px` above and below each timing entry. Set one non-negative number to change all entry rows together.
|
|
604
|
-
|
|
605
|
-
The same timing fit values can be passed to `mountRaceCanvas()` when `includeTimingTower` is enabled. Standalone timing towers are capped by `--timing-board-max-width` and fill their mount root height; placing the root in a fixed-height container makes only the timing entries scroll. Timing entries always stack from the top as compact content-sized rows, so P1/P2 occupy the same vertical positions whether the race has 2 cars or 20. Use `timingEntryVerticalPadding` when the tower needs looser or denser row spacing.
|
|
606
|
-
|
|
607
|
-
The timing tower defaults to `Int` mode and includes a single compact `Int`/`Gap` header toggle unless `ui.timingGapModeToggle: false` is set. The toggle lives in the timing header's gap column and is package-owned: it keeps the same header alignment as `POS`, `TEAM`, `PROJECT`, and `TYRE`, uses a practical `44px` hit target, and draws selected/focus states inside the tower header so the broadcast proportions do not shift. Hosts should not replace this with custom CSS against package internals. If a host needs a different control placement, hide the header toggle and call the controller methods from host-owned UI instead.
|
|
608
|
-
|
|
609
|
-
Hosts can set the initial mode with `ui.timingGapMode` and can change it at any time with `setTimingGapMode('interval' | 'leader')`, `getTimingGapMode()`, or `toggleTimingGapMode()`. `Int` shows the interval to the car ahead, while `Gap` shows total gap to the leader. Seconds gaps are measured from hidden fixed timing-line crossings on the track. If a car is one or more whole laps behind in the selected mode, the tower shows labels such as `+1` or `+2` instead of a seconds estimate.
|
|
610
|
-
|
|
611
|
-
Other UI switches:
|
|
612
|
-
|
|
613
|
-
- `cameraControls` defaults to `'external'`; set it to `'embedded'` only when controls should intentionally appear inside the canvas, or `false` to omit them.
|
|
614
|
-
- `penaltyBanners: true` shows warnings and penalties in the top steward message instead of the project/radio lower-third.
|
|
615
|
-
- `showFps: false` hides the FPS readout.
|
|
616
|
-
- `telemetryIncludesOverview: false` keeps the car/driver overview out of the telemetry stack so hosts can mount `mountCarDriverOverview()` separately.
|
|
617
|
-
- `telemetryModules` controls optional package-owned telemetry surfaces: `core`, `sectors`, `lapTimes`, and `sectorTimes`.
|
|
618
|
-
|
|
619
|
-
Project/radio lower-thirds include a package-owned top-right close button that hides the current pill before its scheduled timeout. Time penalties put a large `+10s` style chip in the left block of the steward message and the affected car/rule beside it. Warning messages use warning colors and do not create timing-row `!` badges. The telemetry surfaces are also available as fully detached mount methods.
|
|
620
|
-
|
|
621
|
-
### Composable overlay layout contract
|
|
622
|
-
|
|
623
|
-
`ui.layoutPreset: 'left-tower-overlay'` is package-owned. Hosts should not recreate or depend on internal shell classes such as `.sim-shell--left-tower-overlay` or `.sim-grid` as their public integration contract.
|
|
624
|
-
|
|
625
|
-
For the all-in-one simulator, call `mountF1Simulator()` and let PaddockJS generate the shell. For composable layouts, hosts should provide mount roots and call package mount methods such as `mountRaceCanvas(root, { includeTimingTower: true, includeRaceDataPanel: true })` or `mountRaceTelemetryDrawer(root)`. Host CSS may size the outer container, but timing-tower placement, gutter measurement, camera safe area, and narrow-screen stacking are owned by PaddockJS.
|
|
626
|
-
|
|
627
|
-
Mounted package surfaces include a lightweight red start-light loading overlay. The runtime removes each overlay after PixiJS, assets, controls, and the initial readouts are ready.
|
|
628
|
-
|
|
629
|
-
The runtime also pauses its render ticker when the race canvas is offscreen or the browser tab is hidden. This keeps pages with multiple PaddockJS embeds responsive without requiring host code to manually start and stop each simulator. At `5x` and `10x` browser playback, noncritical timing/readout DOM refreshes run at a lower cadence while fixed-step simulation and race events keep using authoritative race state.
|
|
630
|
-
|
|
631
|
-
Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; lifecycle callback errors are routed to `onError` when provided and do not stop the simulator loop. `onDriverOpen(driver)` is host-owned navigation code and is not wrapped as a lifecycle callback. Race snapshots include per-car interval timing, leader-gap timing, whole-lap gap counts, calibrated `speedKph`, automatic `track.sectors`, hidden `track.timingLines`, per-car `lapTelemetry`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field is finished or DNF. Sector telemetry clears future-sector values so banners and sidebars show completed splits before the active sector plus the active live timer, not stale later-sector entries. Cars that have crossed the line before full race completion expose `raceStatus: 'waved-flag'` / `wavedFlag: true` and stay frozen in provisional finish order; later barrier contact or stalled-off-track detection does not turn that result into DNF. Destroyed/out-of-race cars expose DNF metadata, show `DNF` at the bottom of the timing tower, render faded/gray in the race canvas, and appear after finishers in final classification with no finish time. If a DNF car is restored before final classification, it re-enters live timing and the race waits for it again; once `raceControl.finished` is true, classification stays final. Final classification applies time penalties and unserved service conversions through `adjustedFinishTime`, then applies position-drop and disqualification consequences to finishers before DNF entries. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
|
|
183
|
+
`npm run check` is the normal local gate: docs checks, fast runtime tests, public declarations, dry package contents, packed-package consumption in a fresh Vite app, the showcase build, and a quick Chromium smoke against the showcase. `npm run check:release` adds slow characterization tests and the full browser smoke matrix.
|
|
632
184
|
|
|
633
185
|
## License
|
|
634
186
|
|