@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/INSTALL_AND_UPDATE.md +21 -4
  3. package/README.md +103 -551
  4. package/docs/architecture.md +28 -18
  5. package/docs/component_inventory.md +5 -4
  6. package/docs/composable-layouts.md +79 -0
  7. package/docs/custom_model_controller.md +4 -4
  8. package/docs/data-helpers.md +50 -0
  9. package/docs/data_contract.md +13 -7
  10. package/docs/getting-started.md +105 -0
  11. package/docs/headless-environment.md +68 -0
  12. package/docs/index.md +17 -4
  13. package/docs/rules.md +6 -2
  14. package/docs/system_specs.md +60 -7
  15. package/docs/theming.md +64 -0
  16. package/docs/training.md +20 -14
  17. package/docs/troubleshooting.md +76 -0
  18. package/docs/upgrading-hosts-to-3.md +42 -0
  19. package/examples/python/README.md +6 -0
  20. package/examples/python/base_policy_server.py +19 -0
  21. package/package.json +21 -5
  22. package/src/api/PaddockSimulatorController.js +78 -22
  23. package/src/api/runtimeTheme.js +59 -0
  24. package/src/app/F1SimulatorApp.js +24 -3
  25. package/src/app/readouts/timingTowerRenderer.js +203 -42
  26. package/src/app/runtime/frameLoop.js +5 -1
  27. package/src/config/defaultOptions.js +8 -1
  28. package/src/config/themeOptions.js +5 -0
  29. package/src/data/index.d.ts +212 -0
  30. package/src/data/index.js +14 -0
  31. package/src/environment/episode.js +11 -1
  32. package/src/environment/metrics.js +59 -16
  33. package/src/environment/observationVector.js +50 -33
  34. package/src/environment/observations.js +89 -20
  35. package/src/environment/runtime.js +41 -0
  36. package/src/environment/sensors/carRays.js +48 -37
  37. package/src/environment/sensors/index.js +501 -97
  38. package/src/environment/sensors/indexedRayBands.js +134 -46
  39. package/src/environment/sensors/rayBandTrace.js +1171 -0
  40. package/src/environment/sensors/rayChannels.js +108 -0
  41. package/src/environment/sensors/rayConfig.js +71 -25
  42. package/src/environment/sensors/rayGeometry.js +23 -17
  43. package/src/environment/sensors/sensorTargets.js +67 -1
  44. package/src/environment/sensors/surfaceRays.js +130 -89
  45. package/src/environment/sensors/trackRays.js +115 -1
  46. package/src/index.d.ts +43 -0
  47. package/src/index.js +27 -2
  48. package/src/placeholder/index.d.ts +11 -0
  49. package/src/placeholder/index.js +61 -0
  50. package/src/placeholder.css +72 -0
  51. package/src/simulation/collisionGeometry.js +260 -96
  52. package/src/simulation/pit/pitFlow.js +21 -9
  53. package/src/simulation/pit/pitQueue.js +5 -3
  54. package/src/simulation/pit/pitRouteMovement.js +9 -4
  55. package/src/simulation/pit/pitRouting.js +77 -42
  56. package/src/simulation/pit/pitState.js +1 -0
  57. package/src/simulation/pit/pitTireService.js +7 -3
  58. package/src/simulation/race/raceFinish.js +25 -8
  59. package/src/simulation/race/raceOrder.js +189 -32
  60. package/src/simulation/race/raceProgress.js +129 -37
  61. package/src/simulation/race/raceSetup.js +2 -0
  62. package/src/simulation/race/raceStep.js +25 -13
  63. package/src/simulation/raceSimulation.js +20 -17
  64. package/src/simulation/rules/collisionSteward.js +40 -23
  65. package/src/simulation/rules/rulesReview.js +77 -56
  66. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  67. package/src/simulation/timing/drsTiming.js +143 -30
  68. package/src/simulation/timing/gapEstimation.js +25 -3
  69. package/src/simulation/timing/lapTelemetry.js +115 -36
  70. package/src/simulation/timing/raceTiming.js +5 -1
  71. package/src/simulation/timing/sectorPerformance.js +104 -26
  72. package/src/simulation/timing/timingHistory.js +229 -14
  73. package/src/simulation/timing/timingLines.js +65 -7
  74. package/src/simulation/track/pitLaneLayout.js +23 -0
  75. package/src/simulation/track/pitLaneState.js +218 -19
  76. package/src/simulation/track/spatialQueries.js +67 -11
  77. package/src/simulation/track/trackMath.js +91 -24
  78. package/src/simulation/track/trackModel.js +2 -0
  79. package/src/simulation/track/trackQueryIndex.js +1334 -100
  80. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  81. package/src/simulation/track/trackQueryScratch.js +7 -0
  82. package/src/simulation/track/trackQueryStats.js +25 -0
  83. package/src/simulation/track/trackSnapshotJson.js +139 -0
  84. package/src/simulation/track/trackStatePolicy.js +249 -1
  85. package/src/simulation/vehicle/contactResolution.js +96 -59
  86. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  87. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  88. package/src/simulation/vehicle/runoffResponse.js +54 -29
  89. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  90. package/src/simulation/vehicle/vehiclePhysics.js +42 -27
  91. package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
  92. package/src/simulation/vehicle/vehicleState.js +6 -0
  93. package/src/simulation/vehicle/wheelSurface.js +807 -92
  94. package/src/styles.css +497 -497
  95. package/src/ui/raceControlsTemplate.js +1 -0
  96. package/src/ui/telemetryDrawerTemplate.js +1 -0
  97. package/src/ui/telemetryTemplates.js +1 -0
@@ -0,0 +1,105 @@
1
+ # Getting Started
2
+
3
+ Use this guide when installing PaddockJS into a normal browser host website.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @inventure71/paddockjs
9
+ ```
10
+
11
+ PaddockJS is a browser component package. A host page gives it a root element, project/driver data, and optional routing callbacks. PaddockJS owns the simulator shell, PixiJS renderer, CSS, default assets, race controls, timing tower, telemetry surfaces, and simulation runtime.
12
+
13
+ ## Mount The Simulator
14
+
15
+ ```html
16
+ <div id="f1-simulator-root"></div>
17
+ ```
18
+
19
+ ```js
20
+ import { mountF1Simulator } from '@inventure71/paddockjs';
21
+
22
+ const root = document.getElementById('f1-simulator-root');
23
+
24
+ if (root) {
25
+ await mountF1Simulator(root, {
26
+ drivers,
27
+ entries,
28
+ title: 'F1 Simulator Lab',
29
+ kicker: 'Race Control',
30
+ backLinkHref: '/projects',
31
+ backLinkLabel: 'Projects',
32
+ onDriverOpen(driver) {
33
+ if (driver.link) window.location.href = driver.link;
34
+ },
35
+ });
36
+ }
37
+ ```
38
+
39
+ Use the root package only for browser mounts and browser helper exports. It imports package CSS and bundled simulator assets.
40
+
41
+ ## Startup Loading
42
+
43
+ `mountF1Simulator()` replaces the host root with a lightweight PaddockJS shell immediately after the package JavaScript starts running. The shell is normal HTML/CSS with package loading overlays; the PixiJS renderer, simulator runtime, textures, controls, and initial readouts are initialized afterward. The mount promise resolves only after that runtime initialization is complete.
44
+
45
+ For the fastest visible first paint, render the tiny placeholder before loading the full simulator bundle:
46
+
47
+ ```html
48
+ <section class="simulator-section">
49
+ <div id="f1-simulator-root"></div>
50
+ </section>
51
+ ```
52
+
53
+ ```js
54
+ import { createPaddockLoadingPlaceholder } from '@inventure71/paddockjs/placeholder';
55
+ import '@inventure71/paddockjs/placeholder.css';
56
+
57
+ const root = document.getElementById('f1-simulator-root');
58
+ root.innerHTML = createPaddockLoadingPlaceholder({
59
+ label: 'Loading simulator',
60
+ detail: 'Preparing race control',
61
+ });
62
+
63
+ const { mountF1Simulator } = await import('@inventure71/paddockjs');
64
+ await mountF1Simulator(root, { drivers, entries });
65
+ ```
66
+
67
+ `@inventure71/paddockjs/placeholder` is CSS-free JavaScript and `@inventure71/paddockjs/placeholder.css` is a standalone tiny stylesheet. They do not import PixiJS, the simulator runtime, bundled assets, or the full package stylesheet. The default placeholder uses the same start-light idea as the simulator loading overlay. Pass `variant: 'custom'`, `className`, `detail`, or safe `attributes` when the host wants to layer its own overlay while keeping the same minimal helper contract.
68
+
69
+ That placeholder covers the time before the full PaddockJS bundle executes. Once the bundle runs, PaddockJS owns the root markup and shows its package loading overlays until the first real simulator frame is ready.
70
+
71
+ ## CSS
72
+
73
+ Most browser bundlers consume the root package stylesheet automatically. If your host build does not, import the stylesheet explicitly from a browser entry:
74
+
75
+ ```js
76
+ import '@inventure71/paddockjs/styles.css';
77
+ ```
78
+
79
+ Package CSS is scoped to PaddockJS mount roots/components. It uses system font stacks and does not load Google Fonts or any remote font. Hosts that want branded typography should load fonts in the host app.
80
+
81
+ Do not target package internals such as `.sim-grid`, `.timing-list`, `.race-data-panel`, `.camera-controls`, or `.start-lights` from host CSS. Size the mount root; let PaddockJS own internal layout.
82
+
83
+ ## Assets
84
+
85
+ Hosts should not copy simulator assets into their own source tree. PaddockJS ships its default car, safety-car, logo, panel, and track texture assets. Browser bundlers such as Vite, Rollup, and Webpack bundle those assets from the package.
86
+
87
+ ## Container Sizing
88
+
89
+ PaddockJS supports mount roots at least `320px` wide. Use normal responsive host CSS:
90
+
91
+ ```css
92
+ .simulator-section {
93
+ width: 100%;
94
+ min-width: 0;
95
+ }
96
+ ```
97
+
98
+ If a package surface is constrained below its supported size, PaddockJS shows an `Unsupported size` placeholder rather than clipping controls or overlapping race UI.
99
+
100
+ ## Where To Go Next
101
+
102
+ - Use [Composable Layouts](composable-layouts.md) when the host needs separate package-owned surfaces.
103
+ - Use [Theming](theming.md) for runtime light/dark sync and token customization.
104
+ - Use [Data Contract](data_contract.md) for full driver, entry, rule, UI, callback, and asset option shapes.
105
+ - Use [Troubleshooting](troubleshooting.md) when a host build or browser smoke fails.
@@ -0,0 +1,68 @@
1
+ # Headless Environment
2
+
3
+ Use the environment subpath for browser-free simulation, JavaScript training loops, evaluation, and worker bridges.
4
+
5
+ ```js
6
+ import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
7
+ ```
8
+
9
+ Do not import the root package from raw Node or headless tooling. The root package is a browser component entry and imports CSS and image assets.
10
+
11
+ ## Basic Loop
12
+
13
+ ```js
14
+ const env = createPaddockEnvironment({
15
+ drivers,
16
+ entries,
17
+ controlledDrivers: ['budget'],
18
+ frameSkip: 4,
19
+ physicsMode: 'advanced',
20
+ });
21
+
22
+ let result = env.reset();
23
+ result = env.step({
24
+ budget: { steering: 0, throttle: 1, brake: 0 },
25
+ });
26
+
27
+ env.destroy();
28
+ ```
29
+
30
+ `controlledDrivers` is required. Controlled actions use normalized `steering`, `throttle`, and `brake`, plus optional `pitIntent` and `pitCompound`.
31
+
32
+ ## Training Boundary
33
+
34
+ PaddockJS owns deterministic stepping, observations, action validation, specs, neutral metrics, scenario reset placement, browser playback hooks, and optional reward callback plumbing. The user owns the ML framework, model weights, reward formula, storage, checkpoints, logs, and training algorithm.
35
+
36
+ For realistic local-perception policies, prefer:
37
+
38
+ ```js
39
+ const env = createPaddockEnvironment({
40
+ drivers,
41
+ entries,
42
+ controlledDrivers: ['budget'],
43
+ physicsMode: 'advanced',
44
+ observation: { profile: 'physical-driver' },
45
+ });
46
+ ```
47
+
48
+ Use `env.getActionSpec()` and `env.getObservationSpec()` instead of guessing action ranges or vector fields.
49
+
50
+ ## Browser Expert Mode
51
+
52
+ Browser expert mode is opt-in through the browser mount API and is for visual policy playback/debugging:
53
+
54
+ ```js
55
+ const simulator = await mountF1Simulator(root, {
56
+ drivers,
57
+ entries,
58
+ expert: {
59
+ enabled: true,
60
+ controlledDrivers: ['budget'],
61
+ frameSkip: 4,
62
+ },
63
+ });
64
+ ```
65
+
66
+ When expert mode is enabled, the visual simulator advances only when host code calls `simulator.expert.step(actions)`. Do not enable expert mode in basic consumer examples; it is easy to think the simulator is broken because it is intentionally waiting for model actions.
67
+
68
+ For the full contract, see [Bring Your Own Model](training.md), [Custom Model Controller Guide](custom_model_controller.md), and [Model Sense Contract](sense_contract.md).
package/docs/index.md CHANGED
@@ -2,7 +2,19 @@
2
2
 
3
3
  PaddockJS is a browser-mounted, data-driven F1-style race simulator component. It is designed to live as its own package and be installed into host websites such as the portfolio site.
4
4
 
5
- Use these docs as the package source of truth:
5
+ Code snippets in the documentation are illustrative host integration fragments unless a page says they are a complete runnable file. Public import paths are still authoritative.
6
+
7
+ Start with the consumer task docs:
8
+
9
+ - [Getting Started](getting-started.md): install, browser mount, package CSS, bundled assets, host responsibilities, and container sizing.
10
+ - [Composable Layouts](composable-layouts.md): building host layouts from package-owned simulator surfaces.
11
+ - [Headless Environment](headless-environment.md): browser-free environment imports, expert-mode boundary, and basic stepping.
12
+ - [Data Helpers](data-helpers.md): CSS-free data/tooling imports and procedural track helpers.
13
+ - [Theming](theming.md): mount-time theme, runtime theme sync, and public theme helpers.
14
+ - [Troubleshooting](troubleshooting.md): common host build, CSS, asset, expert-mode, and upgrade issues.
15
+ - [Upgrading Hosts to 4.x](upgrading-hosts-to-3.md): short checklist for moving a host website from 2.x-era usage to the 4.x package API.
16
+
17
+ Reference docs:
6
18
 
7
19
  - [System Specs](system_specs.md): expected package behavior, public API, runtime guarantees, and verification standards.
8
20
  - [Rules](rules.md): race-control behavior, DRS, safety car, starts, ordering, contact handling, and simulation rules.
@@ -18,10 +30,11 @@ Use these docs as the package source of truth:
18
30
 
19
31
  Common paths:
20
32
 
21
- - Installing PaddockJS into a browser host: read [System Specs](system_specs.md), then [Data Contract](data_contract.md), then [INSTALL_AND_UPDATE.md](../INSTALL_AND_UPDATE.md).
22
- - Building a package-owned composable layout: read the public API, returned-controller, and layout support contract sections in [System Specs](system_specs.md), then the sizing and UI options in [Data Contract](data_contract.md).
33
+ - Installing PaddockJS into a browser host: read [Getting Started](getting-started.md), then [Data Contract](data_contract.md) for full option shapes.
34
+ - Upgrading an existing browser host from 2.x-era usage: read [Upgrading Hosts to 4.x](upgrading-hosts-to-3.md), then [Troubleshooting](troubleshooting.md) if the host build fails.
35
+ - Building a package-owned composable layout: read [Composable Layouts](composable-layouts.md), then the sizing and UI options in [Data Contract](data_contract.md).
23
36
  - Enabling pits, penalties, tire requirements, safety car, or stalled off-track DNF: read [Rules](rules.md) first. Pit stops require `rules.modules.pitStops.enabled`; `tireStrategy` alone only controls compounds, tire-requirement stewarding, and pit target choices.
24
- - Connecting a trained model: read [Bring Your Own Model](training.md), then [Custom Model Controller Guide](custom_model_controller.md). PaddockJS owns observations, actions, specs, scenarios, and browser playback; the host owns the model, reward, storage, and training framework.
37
+ - Connecting a trained model: read [Headless Environment](headless-environment.md), [Bring Your Own Model](training.md), then [Custom Model Controller Guide](custom_model_controller.md). PaddockJS owns observations, actions, specs, scenarios, and browser playback; the host owns the model, reward, storage, and training framework.
25
38
  - Changing internals: read [Architecture](architecture.md) before editing source so new code lands in the feature-owned module instead of a facade or compatibility barrel.
26
39
 
27
40
  When simulator behavior changes, update the relevant doc in this folder in the same change.
package/docs/rules.md CHANGED
@@ -103,7 +103,7 @@ The current implementation normalizes and exposes all module config, records a p
103
103
 
104
104
  Participant interaction profiles can opt real cars out of specific interaction systems without turning them into replay ghosts. A non-colliding profile skips vehicle collision resolution and collision stewarding for that car pair, but the car still uses normal steering/throttle/brake physics. A non-blocking pit profile is ignored by pit service occupancy and queue-blocking checks. A car with `affectsRaceOrder: false` remains in `snapshot.cars` but is excluded from ranking, DRS references, finish order, and final classification. Replay ghosts are separate trajectory overlays in `snapshot.replayGhosts`; they never participate in rules, collisions, timing, pit stops, or penalties.
105
105
 
106
- Automatic pit routing owns car motion while a car is entering, queued, servicing, or exiting. During those phases the route/service logic keeps scalar speed, throttle/brake, `appliedControls`, and advanced telemetry synchronized so advanced-mode observations and the next physics-owned step do not inherit stale pre-pit motion, grip, slip, or instability state. `velocityX/Y` is synchronized in advanced mode and cleared from arcade snapshots because arcade physics does not own a world-velocity state.
106
+ Automatic pit routing owns car motion while a car is entering, queued, servicing, or exiting. During those phases the route/service logic keeps scalar speed, throttle/brake, `appliedControls`, and advanced telemetry synchronized so advanced-mode observations and the next physics-owned step do not inherit stale pre-pit motion, grip, slip, or instability state. Route-owned sampling/projection scratch preserves the same route geometry and limiter behavior without allocating sampled point/projection objects during each transition tick. `velocityX/Y` is synchronized in advanced mode and cleared from arcade snapshots because arcade physics does not own a world-velocity state.
107
107
 
108
108
  ## Steward Strictness
109
109
 
@@ -177,6 +177,10 @@ Race order is based on `raceDistance`, descending. Ties fall back to original dr
177
177
 
178
178
  During safety car, order is frozen at deployment time. That prevents passing from reshuffling the timing tower while the safety car is active.
179
179
 
180
+ During normal green-flag stepping, driver decisions read the previously committed order, gap, DRS, and rank state. The current step then moves cars, applies local physics/pit/collision/rule phases, and commits the next broad race state once at the end of the active step. This means AI reactions can be one fixed frame behind the most recent physical movement, which is intentional for deterministic fixed-step ordering and avoids repeated broad timing/order recalculation inside one step.
181
+
182
+ Advanced-mode surface classification is also step-phased. The pre-physics surface read uses the last committed contact-patch state for grip, runoff response only checks barrier reach and records the center track state it already computed, and the post-collision local refresh owns the final wheel/contact-patch update for that step. If collision correction moves a car after runoff, the recorded center state is discarded and the refresh recomputes from the final pose.
183
+
180
184
  The simulation builds hidden timing lines around every track at an F1-style mini-sector spacing target of roughly `150m..200m`. Timing history is sampled per car and timing-line crossing timestamps are used to calculate:
181
185
 
182
186
  - Interval to the car ahead as `intervalAheadSeconds` / `gapAheadSeconds`.
@@ -319,7 +323,7 @@ Track state can classify a car as on:
319
323
 
320
324
  Surface affects grip, drag, and rolling resistance.
321
325
 
322
- Surface physics is wheel-based. `src/simulation/vehicle/wheelSurface.js` classifies each wheel contact patch against the track model and the car uses the worst current surface for physics: `barrier > gravel > grass > kerb > pit-box > pit-lane > pit-entry/pit-exit > track`. Normal main-track running uses an analytic signed-offset fast path from cached wheel geometry, while pit-lane and pit-connector edge cases use full patch sampling. One wheel on gravel slows the car with gravel behavior even if the other three wheels are on asphalt. In addition, `vehiclePhysics.js` compares left-side and right-side wheel resistance and applies a small capped yaw-rate bias toward the slower side, so asymmetric gravel/grass/kerb contact can tug the car toward the dirty side without becoming a full tire-force simulation. One wheel on kerb reports `kerb` and applies kerb-level grip/drag without causing a track-limit violation by itself. If the car footprint reaches the rendered barrier wall's inner face, every physics mode marks the car destroyed instead of treating the wall as recoverable low-grip road. Pit entry, pit lane, pit exit, and pit-box surfaces remain legal drivable surfaces and do not count toward stalled off-track DNF timing.
326
+ Surface physics is wheel-based. `src/simulation/vehicle/wheelSurface.js` classifies each wheel contact patch against the track model and the car uses the worst current surface for physics: `barrier > gravel > grass > kerb > pit-box > pit-lane > pit-entry/pit-exit > track`. Normal main-track running uses an analytic signed-offset fast path from cached wheel geometry, and pit connectors first resolve wheel centers through local connector geometry before falling back to full patch sampling only for genuinely ambiguous pit-road overlap. One wheel on gravel slows the car with gravel behavior even if the other three wheels are on asphalt. In addition, `vehiclePhysics.js` compares left-side and right-side wheel resistance and applies a small capped yaw-rate bias toward the slower side, so asymmetric gravel/grass/kerb contact can tug the car toward the dirty side without becoming a full tire-force simulation. One wheel on kerb reports `kerb` and applies kerb-level grip/drag without causing a track-limit violation by itself. If the car footprint reaches the rendered barrier wall's inner face, every physics mode marks the car destroyed instead of treating the wall as recoverable low-grip road. Pit entry, pit lane, pit exit, and pit-box surfaces remain legal drivable surfaces and do not count toward stalled off-track DNF timing.
323
327
 
324
328
  Rendered material bands use those same thresholds. The visible gravel band starts at the outer kerb edge and ends at the simulated gravel edge; the visible runoff band starts at the gravel edge and ends at the barrier centerline; the barrier wall is drawn from the same `track.barrierWidth` used by destruction. Rendering, ray surface hits, wheel-surface classification, and barrier destruction must stay on this shared geometry contract.
325
329
 
@@ -34,7 +34,17 @@ import {
34
34
  } from '@inventure71/paddockjs/environment';
35
35
  ```
36
36
 
37
- The environment subpath is the only public headless training import. It must stay free of DOM, PixiJS, CSS, and browser app dependencies.
37
+ CSS-free data/helper import:
38
+
39
+ ```js
40
+ import {
41
+ DriverData,
42
+ normalizeSimulatorDrivers,
43
+ createProceduralTrack,
44
+ } from '@inventure71/paddockjs/data';
45
+ ```
46
+
47
+ The environment subpath is the only public headless training import. The data subpath is for host tooling and browser/server build code that needs data helpers without importing package CSS. Both subpaths must stay free of DOM, PixiJS, CSS, and browser app dependencies.
38
48
 
39
49
  Root package exports:
40
50
 
@@ -42,10 +52,27 @@ Root package exports:
42
52
  | --- | --- | --- |
43
53
  | Browser mounts | `mountF1Simulator`, `createPaddockSimulator` | all-in-one shell or composable package-owned UI surfaces |
44
54
  | Composable helper mounts | `mountRaceControls`, `mountCameraControls`, `mountSafetyCarControl`, `mountTimingTower`, `mountRaceCanvas`, `mountTelemetryPanel`, `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetrySectorBanner`, `mountTelemetryLapTimes`, `mountTelemetrySectorTimes`, `mountRaceTelemetryDrawer`, `mountCarDriverOverview`, `mountRaceDataPanel` | function-call wrappers around a composable simulator controller |
55
+ | Startup placeholder | `createPaddockLoadingPlaceholder` from `@inventure71/paddockjs/placeholder`, plus `@inventure71/paddockjs/placeholder.css` | optional pre-JS loading placeholder that does not import PixiJS, simulator assets, root runtime CSS, or the browser mount |
45
56
  | Data helpers | `DriverData`, `VehicleData`, `CHAMPIONSHIP_ENTRY_BLUEPRINTS`, `DEMO_PROJECT_DRIVERS`, `buildChampionshipDriverGrid`, `formatDriverNumber`, `normalizeSimulatorDrivers` | demo data, host entry normalization, and rating helper construction |
57
+ | Theme helpers | `DEFAULT_PADDOCK_THEME`, `PADDOCK_THEME_CSS_VARIABLES`, `PADDOCK_THEME_TOKEN_KEYS`, `resolvePaddockTheme`, `applyPaddockTheme` | host-side theme preview/sync code without copying package token names |
46
58
  | Package constants | `DEFAULT_F1_SIMULATOR_ASSETS`, `PADDOCK_SIMULATOR_PRESETS` | inspect bundled asset mapping and UI preset defaults |
47
59
  | Track and units | `createProceduralTrack`, `REAL_F1_CAR_LENGTH_METERS`, `VISUAL_CAR_LENGTH_METERS`, `SIM_UNITS_PER_METER`, `TARGET_F1_TOP_SPEED_KPH`, `metersToSimUnits`, `simUnitsToMeters`, `kphToSimSpeed`, `simSpeedToKph`, `simSpeedToMetersPerSecond` | generated track definitions and physical/display unit conversion |
48
- | Controller loop | `createPaddockDriverControllerLoop` | shared browser/headless batched model-controller orchestration |
60
+ | Controller loop | `createPaddockDriverControllerLoop` | compatibility browser export for shared model-controller orchestration; browser-free code should import it from `@inventure71/paddockjs/environment` |
61
+
62
+ Data subpath exports:
63
+
64
+ | Export | Use |
65
+ | --- | --- |
66
+ | `DriverData`, `VehicleData`, `CHAMPIONSHIP_ENTRY_BLUEPRINTS`, `DEMO_PROJECT_DRIVERS`, `buildChampionshipDriverGrid`, `formatDriverNumber`, `normalizeSimulatorDrivers` | CSS-free host data normalization and rating helper construction |
67
+ | `createProceduralTrack` | CSS-free procedural track generation helper |
68
+ | `kphToSimSpeed`, `simSpeedToKph` | CSS-free speed conversion helpers |
69
+
70
+ Placeholder subpath exports:
71
+
72
+ | Export | Use |
73
+ | --- | --- |
74
+ | `createPaddockLoadingPlaceholder(options)` | Generate inert, escaped startup placeholder HTML for the network/import gap before the root simulator bundle runs. |
75
+ | `PaddockLoadingPlaceholderOptions`, `PaddockLoadingPlaceholderVariant` | TypeScript shapes for host placeholder configuration. |
49
76
 
50
77
  Environment subpath exports:
51
78
 
@@ -60,7 +87,9 @@ Environment subpath exports:
60
87
  | `createEnvironmentWorkerProtocol`, `handleEnvironmentMessage` | JSON-serializable worker bridge helpers |
61
88
 
62
89
  `createProgressReward()` is published from the same subpath only as non-canonical demo reward code for examples and smoke tests. It must remain optional and replaceable; environment stepping must continue to work with a custom `reward(context)` callback or no reward callback. Reward callbacks are user-owned formulas over package-owned facts, including neutral per-driver `metrics` and `info.drivers[driverId]` episode state. They run only for `step(actions)` transition results; reset/read paths are reward-neutral. Non-finite or missing callback results are normalized to a neutral `0` reward for that controlled driver.
63
- Ray observations expose track-transition distance and car distance per ray. Track transitions use `kind: 'exit'` when the ray starts on track and reaches the border, `kind: 'entry'` when the ray starts off track and reaches the road again, and `kind: null` with max distance when no transition is visible. Track-position, surface, pit-lane, and ray fallback queries use an internal non-enumerable track query index built with the track model. Browser/expert mounts, headless environments, direct race-simulation construction, and compact training-style environment runs all use the indexed path. This changes runtime cost, not public observation shapes.
90
+ Ray observations expose track-transition distance and car distance per ray. Track transitions use `kind: 'exit'` when the ray starts on track and reaches the border, `kind: 'entry'` when the ray starts off track and reaches the road again, and `kind: null` with max distance when no transition is visible. Driver-precision road-edge, kerb, and illegal-surface channels use direct indexed ray-band intersections when the candidate boundary can be validated against driver track state, including validated off-track recovery boundaries. Barrier-origin recovery rays also stay on the direct indexed path when the tracer can already prove road-edge/kerb misses and the first non-barrier illegal-surface entry. Direct ray-band intersection consumes query-index segment IDs and typed centerline arrays instead of materializing temporary segment objects; the object-returning segment query helper remains for public/debug callers. Scratch-backed internal ray batches reuse the rich ray channel containers for road-edge, kerb, illegal-surface, and car hits, and car-hit footprint checks reuse the caller-owned ray vector plus a per-ray result target instead of recomputing ray geometry; no-scratch public ray calls still return fresh channel objects. Batch-training surface-only ray batches synthesize safe main-road origin state from same-pose `progress`/`signedOffset` geometry and reuse shared track-band boundary plus surface-hit containers, so kerb/illegal-surface channels stay indexed without nearest-track origin classification or per-ray hit-object replacement. Ambiguous recovery, pit-connector, and pit-lane-origin rays that are still not provable from the indexed bands are resolved by one bounded sampled recovery pass inside the ray-band tracer so all requested road/surface channels share the same sampled states. Debug precision uses the same tracer-owned path with additional sampled refinement for diagnostic distances; only missing-index cases use the older sampled fallback path. Track-position, surface, pit-lane, and ray fallback queries use an internal non-enumerable track query index built with the track model. Pit-road state classification reuses indexed route candidate buckets, one polyline projection result, and precomputed cumulative route distance tables for entry, fast-lane, working-lane, and exit roads. Connector-adjacent direct pit-entry/pit-exit override checks reuse that same projection target and route metadata; endpoint-progress shortcuts use precomputed endpoint segment windows, and mid-connector points use bounded indexed route candidates without becoming full pit-lane queries. The runtime track-query benchmark explicitly requires those pit-road probes to stay indexed with reused direct connector scratch, positive endpoint-window connector projections, zero connector full-route projection scans, reused route scratch, precomputed distances, zero cumulative-distance rebuilds, and zero pit fallbacks. Browser/expert mounts, headless environments, direct race-simulation construction, and compact training-style environment runs all use the indexed path. This changes runtime cost, not public observation shapes.
91
+
92
+ Scratch-backed internal ray batches also keep requested-channel dispatch in reusable scalar channel flags owned by the shared ray query. The runtime benchmark contract fails if the common direct sensor-ray path allocates channel `Set` containers or replaces those channel-flag containers between batches.
64
93
  The package must not own model training, model persistence, model registries, or trained policy behavior. The supported contract is: external code reads observations, returns normalized actions, and advances either the headless environment or browser expert mode.
65
94
  The shared expert runtime exposes `getActionSpec()` and `getObservationSpec()` so external code can inspect controlled drivers, action ranges, sensor layout, nearby-car limits, track lookahead fields, and the versioned vector schema before connecting a model. Environment reset may apply deterministic scenario placement presets or explicit placement/traffic layouts; model steps may not mutate position and still use normalized actions only. Neutral rollout recording, deterministic evaluation metrics, and the JSON worker protocol are environment utilities, not training algorithms.
66
95
  `createPaddockDriverControllerLoop({ runtime, controller, actionRepeat })` is the shared public controller orchestrator for browser playback and JavaScript training-style loops. `runtime` may be browser expert mode or a headless environment, and `controller.decideBatch(context)` is called once per policy step for all controlled drivers in stable spec order. The loop lazily resets when scheduled playback starts from a fresh instance, caches `actionSpec` and `observationSpec`, reuses the active observation payload, supports async controllers, forwards actions through the normal `step(actions)` API, and repeats each decision for `actionRepeat` runtime frames. Scheduled playback stops and records `stats.lastError` when controller initialization, decision, stepping, or `onStep` hooks fail. `stop()` cancels pending package-owned scheduler handles and custom scheduler handles that either expose `cancel()` or return browser-style numeric timeout/animation-frame ids. It does not load checkpoints, run neural networks, parse model formats, compute rewards, or mutate vehicle state.
@@ -161,7 +190,7 @@ simulator.setRedFlagDeployed(true);
161
190
 
162
191
  Pit APIs are rule-gated. `setPitIntent()` returns `false` until the active rules enable `rules.modules.pitStops.enabled` and the active track has pit-lane geometry. `tireStrategy` does not enable automatic pit routing by itself; it only controls available compounds, tire requirement stewarding, and pit target choices.
163
192
 
164
- Standalone helper functions are also exported for host code that prefers function calls:
193
+ The controller methods above are the canonical composable API. They keep component mounting, lifecycle, restart, theme syncing, and runtime state on one controller object. Standalone helper functions are also exported as thin wrappers for host code that already uses function-call style:
165
194
 
166
195
  ```js
167
196
  mountRaceControls(root, simulator);
@@ -212,6 +241,10 @@ Returned controller:
212
241
  // Included on both APIs:
213
242
  destroy(),
214
243
  restart(nextOptions),
244
+ setTheme(theme),
245
+ setThemeMode(mode),
246
+ getTheme(),
247
+ syncThemeFrom(element, { attribute, map }),
215
248
  selectDriver(driverId),
216
249
  setSafetyCarDeployed(deployed),
217
250
  setRedFlagDeployed(deployed),
@@ -237,15 +270,19 @@ Returned controller:
237
270
  - Composable mounting can also place camera controls and a safety-car button into separate host roots while keeping package-owned markup.
238
271
  - Composable mounting can embed the timing tower directly inside the race canvas with `mountRaceCanvas(root, { includeTimingTower: true })`.
239
272
  - The race canvas is required before `start()` because PixiJS needs a canvas host.
273
+ - Mount component roots before calling `start()`. A pre-start `restart(nextOptions)` re-resolves options and refreshes already mounted component markup, so non-asset changes such as title, kicker, UI visibility, theme, drivers, entries, seed, or track seed are reflected before `start()` binds the runtime.
274
+ - `mountF1Simulator()` and each composable `mount*()` call must synchronously write lightweight package HTML before awaiting runtime initialization. Those startup templates may contain controls/readout placeholders, but they must not require PixiJS, texture loading, race stepping, or host-provided hidden DOM to be visible.
240
275
  - Timing tower, telemetry, controls, and race-data panels are optional from a runtime safety perspective; omitted panels simply do not render their readouts.
241
276
  - Timing tower entries are compact content-sized rows stacked from the top of the timing list. Row vertical position must be based on rank/index, never distributed by available height or total entry count. `ui.timingEntryVerticalPadding` defaults to `5` and applies that pixel value above and below every row entry.
242
277
  - Timing tower entries display team icons. `ui.timingGapMode` sets the initial display to interval-to-car-ahead timing or direct gap-to-leader timing, and the controller can change it at runtime with `setTimingGapMode()`, `getTimingGapMode()`, or `toggleTimingGapMode()`. The tower shows one compact `Int`/`Gap` header toggle by default. That toggle must stay aligned with the other timing header labels, preserve a practical `44px` target, draw active/focus treatment inside the timing header rather than outside the column, and never change the timing board width. `ui.timingGapModeToggle: false` hides only the manual header affordance; it must not disable the controller API. Seconds gaps are calculated from hidden timing-line crossings, and whole-lap deficits display as `+N`.
243
278
  - Snapshots expose calibrated display units through `speedKph`, `distanceMeters`, and `gapMeters`; internal physics remains in simulator units. Public timer, lap-time, sector-time, gap-time, penalty-time, and service-countdown values are seconds.
244
279
  - Snapshots expose automatic three-sector track metadata through `track.sectors` and per-car lap/sector timing through `car.lapTelemetry`, including completed current-lap sector splits before the active sector, live active-sector elapsed time, per-sector progress, and sector performance classes for overall-best, personal-best, and slower completed sector times. Future-sector timing/progress entries are cleared so banner and sidebar telemetry cannot display stale split values from another lap or state.
245
280
  - Entries can include optional `team` metadata. Team color defaults to car color when omitted.
246
- - Mounted package surfaces show a package-owned red start-light loading overlay until `start()` finishes PixiJS, asset, control, and initial readout initialization.
281
+ - Mounted package surfaces show a package-owned red start-light loading overlay until `start()` finishes PixiJS, asset, control, initial readout, and first-frame initialization. `F1SimulatorApp.completeComponentLoading()` marks the owning `data-paddock-component` as loaded and removes `data-paddock-loading` nodes after the initial frame is rendered.
282
+ - Package loading overlays are a post-JavaScript client startup state, not a server-rendered skeleton. Before the root PaddockJS bundle has downloaded, parsed, and called a mount function, only pre-existing HTML/CSS can paint. The optional `@inventure71/paddockjs/placeholder` subpath and `@inventure71/paddockjs/placeholder.css` stylesheet are the package-owned phase-1 contract for that gap. They must stay independent of PixiJS, simulator assets, root package CSS, app runtime modules, and browser mount side effects.
247
283
  - `preset` is a preset-first API. Presets are resolved before explicit host overrides so hosts can use `dashboard`, `timing-overlay`, `compact-race`, or `full-dashboard` as a starting point and still override specific `ui` or `theme` fields.
248
284
  - `theme` is the public sizing/color contract. It resolves semantic package-owned tokens into complete light/dark themes, supports named derivative theme packages through `theme.themes`, selects the active package with `theme.use`, applies component-specific packages through `theme.componentThemes`, and accepts team selectors through `theme.teamThemes` or `entries[*].team.theme`. Component theme keys accept package component ids such as `timing-tower` and camelCase aliases such as `timingTower`; `selectedDriverPanel` targets both selected-driver package surfaces. Selected-driver surfaces use the selected team's theme by default when one is available while structural controls keep the active global theme unless explicitly overridden. Unknown theme tokens or component slots are ignored, one-sided light/dark token overrides generate and cache the opposite mode, and theme plus driver/team colors are validated before they are written to CSS variables. Legacy aliases such as `accentColor`, `greenColor`, and `yellowColor` remain migration aliases for the new semantic tokens.
285
+ - `setTheme(theme)`, `setThemeMode(mode)`, and `syncThemeFrom(element, options)` are runtime presentation APIs. They must resolve through the same theme normalizer used at mount time, reapply CSS variables to every package-owned mounted root, and must not call `restart()` or replace race state. Existing simulation state, timing order, selected driver, active penalties, camera state, speed setting, and pit intent state remain owned by the running app. `syncThemeFrom()` applies immediately, watches the configured attribute with `MutationObserver` when available, maps host attribute values through `options.map`, and returns a cleanup function.
249
286
  - `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 with package-owned padding and pit-lane extent. The driver camera follows the selected car from a lower screen anchor and rotates the world so the selected car points upward; its control is opt-in through `ui.driverCamera: true`, and `initialCameraMode: 'driver'` enables that control automatically. The pit camera frames the operational `track.pitLane` lane, boxes, service areas, and queue areas instead of the longer entry/exit access roads, zooms out when needed to keep that pit-lane work area inside the active race-view safe area, and its control is hidden/disabled if the active track has no pit lane. Zoom buttons and wheel zoom work in every camera mode, including overview, show-all, driver, and pit, but cannot zoom farther out than the active track frame.
250
287
  - Camera controls default to external placement so they do not cover the race view. They can still be embedded in the race canvas by setting `ui.cameraControls: 'embedded'`, externally mounted, or omitted with `false`. Generated camera controls include a `Mute banners` toggle that is off by default and temporarily suppresses project/radio lower-thirds while active. The driver camera button is omitted unless `ui.driverCamera` is active.
251
288
  - Telemetry surfaces are detached package components: core scalar readouts, sector graph, broadcast sector banner, lap-time table, and sector-time table. The broadcast sector banner shows the selected car identity, uses the selected car color for its frame/label, and keeps sector performance colors inside the sector bars. It is an explicitly mounted independent surface, not the default telemetry-drawer lower-third. `mountTelemetryPanel()` is the stack template around those detached pieces, owns vertical scrolling when constrained, and `ui.telemetryModules` controls which pieces appear in stack/drawer templates.
@@ -277,6 +314,7 @@ Returned controller:
277
314
  - The browser UI can opt into top steward messages with `ui.penaltyBanners` and timing-row penalty badges with `ui.timingPenaltyBadges`. Steward messages render track-limit warning events and penalty decisions from the simulation; time-penalty messages put the penalty seconds in a large left chip and use penalty-colored backgrounds, while warnings use warning-colored backgrounds. Timing-row `!` badges are rendered only from `snapshot.penalties`; warning events do not count. UI code must not recalculate steward decisions.
278
315
  - The simulator must stay interactive after being installed through `npm install @inventure71/paddockjs`.
279
316
  - The package must build correctly through a browser bundler that supports JavaScript modules, CSS imports, and image imports.
317
+ - Package CSS must be scoped to package mount roots/components and must not style same-named host elements outside `.f1-sim-component` roots. Generic simulator classes such as `.sim-control`, `.camera-controls`, `.timing-list`, `.telemetry-header`, and `.start-lights` are package internals, not host styling hooks. The stylesheet uses system font stacks by default and must not force remote font requests; hosts that want custom typography should load those fonts themselves and pass theme/CSS-variable overrides.
280
318
  - The simulation should remain deterministic for the same seed, track seed, drivers, entries, and rules.
281
319
  - Warmup is enabled by default for browser mounts, headless environments, and direct simulation creation. It runs on a disposable runtime and must not mutate the visible initial race state; under the default `warmup.policy: 'config-change'`, identical configuration fingerprints reuse cached warmup while seed/config changes rerun warmup automatically.
282
320
  - When `trackSeed` is omitted in a browser mount, the simulator creates a fresh procedural circuit for that mount. Explicit `trackSeed` values are deterministic and cached by seed plus resolved generation options for repeated mounts. Cached procedural definitions are immutable; callers that import `createProceduralTrack(seed, options)` should treat the returned definition as read-only and clone it before custom mutation. `trackGeneration` forwards the same procedural options used by `createProceduralTrack(seed, options)`: `profile`, `length`, `startStraight`, `pitLane`, `shape`, `validation`, and `attempts`. The `race` profile preserves the default full circuit with pit lane; `training-short`, `training-medium`, and `training-technical` are smaller pitless presets intended for training or demos. Explicit option fields override profile defaults after the profile is resolved. Procedural generation traces seeded connected region boundaries, smooths and warps them into centerline controls, then rejects circuits with excessive local heading jumps, turn accumulation, self-intersections, poor clearance, invalid length, or weak shape variation.
@@ -287,7 +325,8 @@ Returned controller:
287
325
  - Expert environment code can create a headless `createPaddockEnvironment()` from the `@inventure71/paddockjs/environment` subpath. It requires explicit `controlledDrivers`, accepts normalized actions `{ steering, throttle, brake }` plus optional `pitIntent` and `pitCompound`, advances only through `step(actions)`, and returns environment-loop JavaScript results with `observation`, `reward`, `metrics`, `terminated`, `truncated`, `done`, `events`, `state`, and `info`. `steering` is an absolute normalized steering target: `-1` points at maximum left, `0` points at center, `1` points at maximum right, and intermediate values are percentages of the maximum steering angle; the vehicle integrator rate-limits motion toward that target instead of snapping the wheel. `steering`, `throttle`, and `brake` are required for each controlled-driver action. Strict action policy throws on missing or non-finite vehicle controls, while report policy records the error and releases stale manual controls so the built-in driver owns that physics step. `info.drivers[driverId]` exposes per-driver `terminated`, `truncated`, `endReason`, `episodeStep`, and `episodeId` for batched loops; selected-driver resets clear only selected driver truncation/termination and do not rewind global `info.step`. For scoped reset results, `info.controlledDrivers` matches the drivers present in `observation` and `metrics`, while `info.drivers` preserves episode state for all configured controlled drivers. Controlled drivers do not receive tire-threshold automatic pit calls; they request automatic pit service through `pitIntent`, may choose the target tire through `pitCompound`, and observe pit-lane/service/race-control state in `observation[driverId].object.self` and `.race`. `pitIntent: 0` is a no-op clear request and does not fail when pit stops are disabled. `self.onTrack` follows wheel-level legality, so track, kerb, and legal pit-lane/box surfaces are on-track while gravel/grass/barrier are off-track. Barrier-wall contact in both physics modes is measured against the rendered wall's inner face; contact marks the car destroyed/DNF, removes it from active collision/sensor participation, emits a `car-destroyed` event, and terminates that driver's episode with `endReason: 'destroyed'`. Opt-in stalled off-track DNF removes the car from the same active participation systems while keeping `destroyed: false` and terminates controlled-driver episodes with `endReason: 'stalled-off-track'` when `rules.modules.stalledDnf.enabled` is `true`. Reset is still an episode-boundary `resetDrivers()` call. Environment scenarios may set reset positions with `preset`, `placements`, and relative `traffic`; `resetDrivers(placements)` may reset selected controlled drivers between episodes without recreating the whole simulation. Partial `reset(options)` calls preserve omitted nested option groups while replacing arrays and explicit scenario placement maps. After placement, selected cars are classified by the same runoff/barrier rules before observations are returned, so cars already inside terminal barrier space report destroyed metrics and stable miss-valued rays instead of doing alive-car far-out ray scans. Both scenario placement and `resetDrivers()` are setup data, not policy actions. `participantInteractions` may make real physics cars non-colliding, sensor-hidden, non-blocking for pit occupancy, or excluded from race order, but those cars still live in `snapshot.cars` and move only through normal physics/control APIs. The `batch-training` profile is non-colliding, sensor-hidden, pit-non-blocking, excluded from race order, and still rendered. `replayGhosts` are separate trajectory-driven entities in `snapshot.replayGhosts`; they are visual/reference overlays and never enter car physics, timing, pit, order, or penalty systems. Replay ghosts are sensor-hidden unless their own sensor flags explicitly opt them into ray or nearby observations.
288
326
  - Headless environments also accept an optional `externalRenderer` observer hook. It receives `{ snapshot, observation, meta }` on `reset`, `step`, and `resetDrivers`, so external tooling can stream authoritative frames to visualizers without letting that transport mutate simulation state.
289
327
  - Controller loop code should use `createPaddockDriverControllerLoop()` instead of duplicating browser/headless stepping. Its controller context groups observations by controlled driver id, includes `orderedObservations` for direct batch tensor assembly, exposes metrics, events, cached specs, and reset-driver ids, and keeps per-driver resets as controller reset hooks when the runtime supports `resetDrivers()`. `previousActions` is the last action map actually applied before the current `actions`; on the first controlled frame after reset it is empty, and after repeated frames it reflects the prior physics step rather than pre-copying the current decision.
290
- - Expert ray sensors originate from the controlled car center. The default compact set is `[-135, -60, -20, 0, 20, 60, 135, 180]`, giving forward, side, and rear awareness while staying small. Rays detect track edges against the actual track geometry and detect car hits by ray-to-car-footprint intersection. Hosts may opt into per-ray lengths, predefined layouts such as `driver-front-heavy`, and surface-aware channels for kerb and illegal surface detection; those channels are computed only when requested. `precision: 'driver'` road-edge, kerb, and illegal-surface channels use the canonical indexed track projection path with driver-precision validation, preserving model-facing distances without substituting debug-only refinement. Barrier walls are rendered/physical terminal boundaries, not model-facing ray targets; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Ray `precision` defaults to `driver`, which is the active model-facing sensor contract; `debug` precision is only for explicitly labeled diagnostics. `observation.profile: 'physical-driver'` exposes local driver-like yaw, contact-patch, boundary, richer ray, and opponent-radar senses while omitting default lookahead samples unless explicitly configured. `observation.output` can be `full`, `vector`, or `object`, `includeSchema: false` omits repeated schema payloads, and `vectorType: 'float32'` returns typed vector buffers for high-throughput JavaScript loops. `result.stateOutput` can be `full`, `minimal`, or `none`; `none` returns `state: null` and is intended for loops that rely on observations, metrics, and info instead of full snapshots.
328
+ - Expert ray sensors originate from the controlled car center. The default compact set is `[-135, -60, -20, 0, 20, 60, 135, 180]`, giving forward, side, and rear awareness while staying small. Rays detect track edges against the actual track geometry and detect car hits by ray-to-car-footprint intersection; scratch-backed car-hit checks use the same caller-owned ray vector as the road/surface tracer and scalar local footprint math. Hosts may opt into per-ray lengths, predefined layouts such as `driver-front-heavy`, and surface-aware channels for kerb and illegal surface detection; those channels are computed only when requested. `precision: 'driver'` road-edge, kerb, and illegal-surface channels use validated direct indexed ray-band intersections for safe normal-driver main-track geometry and validated off-track recovery boundaries; the `batch-training` profile keeps the accelerated direct indexed contract for safe main-track rays, including surface-only batches that reuse shared surface-hit and track-band boundary scratch. Ambiguous recovery, pit-connector, and pit-lane-origin rays use bounded sampled validation inside the ray-band tracer, and `precision: 'debug'` uses that same tracer path with extra sampled refinement for diagnostics. Only missing-index cases use the older sampled fallback path. This preserves model-facing distances without substituting debug-only refinement. Barrier walls are rendered/physical terminal boundaries, not model-facing ray targets; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Ray `precision` defaults to `driver`, which is the active model-facing sensor contract; `debug` precision is only for explicitly labeled diagnostics. `observation.profile: 'physical-driver'` exposes local driver-like yaw, contact-patch, boundary, richer ray, and opponent-radar senses while omitting default lookahead samples unless explicitly configured. `observation.output` can be `full`, `vector`, or `object`, `includeSchema: false` omits repeated schema payloads, and `vectorType: 'float32'` returns typed vector buffers for high-throughput JavaScript loops. `result.stateOutput` can be `full`, `minimal`, or `none`; `none` returns `state: null` and is intended for loops that rely on observations, metrics, and info instead of full snapshots.
329
+ - Scratch-backed expert ray batches dispatch those requested channels through reusable scalar flags on the shared ray query. This is an internal runtime contract only; public ray objects still expose the same road-edge, kerb, illegal-surface, and car channel objects.
291
330
  - Track surface bands are single-source geometry. Rendering derives visible gravel, runoff, and barrier offsets from the same track widths used by wheel surfaces and ray surface hits: road edge, kerb outer edge, gravel outer edge, runoff outer edge, and barrier wall width. New code must not add independent magic offsets for these bands.
292
331
  - Browser expert mode is opt-in with `expert: { enabled: true, controlledDrivers, frameSkip }`. It shares the environment episode lifecycle. The default episode horizon is a high safety cap (`1_000_000` expert steps, roughly 4.6 hours at 60Hz with `frameSkip: 1`) so normal browser sessions should end by race finish instead of max-step truncation. Bounded training, evaluation, and smoke loops should pass an explicit smaller `episode.maxSteps`; `episode.endOnRaceFinish` controls whether race finish is terminal. When enabled, the returned controller exposes `expert.reset()`, `expert.step(actions)`, `expert.getObservation()`, and `expert.getState()`.
293
332
  - Browser expert mode also exposes `attachExternalRenderer(source)`, `detachExternalRenderer()`, and `getExternalRendererState()` for render-only external-frame playback. The `source` callback contract is `subscribe(onFrame) => unsubscribe`, where each frame is `{ snapshot, observation, meta? }`.
@@ -295,6 +334,7 @@ Returned controller:
295
334
  - Browser expert mode disables automatic ticker-driven simulation advancement. The visual canvas updates only after explicit expert `reset()` or `step(actions)` calls.
296
335
  - While external renderer mode is attached, browser expert runtime is strict render-only and rejects local `step()`, `resetDrivers()`, and `reset()`. Hosts must detach before returning to local stepping; detach restores the current local simulation track surface if external frames had redrawn the shared track asset.
297
336
  - Policy Runner `Live preview stream` mode accepts `ws://`/`wss://` push streams and `http://`/`https://` polling endpoints that return `preview:snapshot` frames.
337
+ - Policy Runner `Policy server` mode uses compact HTTP protocol version `2`. Reset/init requests include static specs, configuration, `previousActionFields`, and `metricFields`; per-decision `/policy/decide-batch` requests include `driverIds` plus aligned compact `vectors`, `previousActions`, `metrics`, and `events` arrays only. There is no rich per-decision compatibility toggle in the local-preview policy-server controller; server integrations must read vector inputs by matching `driverIds[index]` to `vectors[index]` and cache the tuple field order received on reset. Full public snapshots remain available through explicit host/debug/export/state APIs, not as the policy-server transport default.
298
338
  - Browser expert mode may opt into `expert.visualizeSensors: true` or `expert.visualizeSensors: { rays: true }`. When enabled, ray sensors render in the race canvas world layer from the selected controlled car by default, using the same observation result produced by explicit expert steps. The overlay draws a separate colored marker for each detected active channel on each ray, including road-edge, kerb, illegal surface, and car hits. Barrier walls are visible track geometry, not ray-hit markers, and browser components must not reintroduce a barrier ray label. Hosts may request `visualizeSensors: { rays: true, drivers: 'all' }` for the heavier all-controlled-car overlay or pass an explicit driver id list. Every model-facing sense follows this rule: the model receives the active environment observation, and Policy Runner/expert visualization displays that same observation instead of recomputing a more precise or different browser-only value. Extra diagnostics must be clearly separated from model senses.
299
339
 
300
340
  ## Current Visible Features
@@ -318,7 +358,7 @@ Returned controller:
318
358
  - Race-finish winner banner and final top-three classification.
319
359
  - `Open project` button driven by `onDriverOpen(driver)`.
320
360
 
321
- Full public race snapshots include `car.trackState` as the serialized car-center track classification. The stable fields are `distance`, `signedOffset`, `crossTrackError`, `surface`, `inPitLane`, `pitLanePart`, `pitBoxId`, `curvature`, and `heading`; pit-specific fields are `null` when not active. Runtime readouts normalize missing optional snapshot arrays and race-control groups before rendering, so partial startup or external-renderer frames do not break the package-owned UI.
361
+ Full public race snapshots include `car.trackState` as the serialized car-center track classification. The stable fields are `distance`, `signedOffset`, `crossTrackError`, `surface`, `inPitLane`, `pitLanePart`, `pitBoxId`, `curvature`, and `heading`; pit-specific fields are `null` when not active. In-memory `snapshot.track` remains the live runtime geometry object, but JSON serialization of that track uses a cached rounded public view, exports decimated `track.samples` rows through `track.sampleSchema`, and omits engine-only pit query helpers such as `pitLane.bounds`, `pitLane.boxBounds`, `pitLane.connectorBounds`, pit-crew metadata, and duplicated pit-box/service-area team fields so full-state export/stream paths do not stringify internal geometry helpers or redundant static pit metadata. Runtime readouts normalize missing optional snapshot arrays and race-control groups before rendering, so partial startup or external-renderer frames do not break the package-owned UI.
322
362
 
323
363
  ## Runtime Requirements
324
364
 
@@ -345,6 +385,19 @@ Expected:
345
385
  - The tracked showcase host builds.
346
386
  - The quick Chromium browser smoke verifies showcase canvas rendering, overflow constraints, one public API action, and live customization theme switching. The release browser smoke verifies the desktop/mobile matrix, package-panel overflow constraints, customization route interactions, public API buttons, and visual policy-runner stepping across generation and race configurations.
347
387
 
388
+ For internal runtime-efficiency development only, also run:
389
+
390
+ ```bash
391
+ npm run benchmark:runtime -- --profile=standard --verify --json
392
+ ```
393
+
394
+ Expected:
395
+
396
+ - The benchmark suite includes simulation stepping, render snapshot creation, timing DOM updates, compact policy-server JSON, and full snapshot JSON characterization.
397
+ - Full snapshot JSON characterization now enforces compact serialized track geometry, including a capped per-snapshot track JSON size after decimating exported track samples, because `snapshot.track` is the dominant static export cost.
398
+ - Compact policy-server transport remains materially smaller and cheaper to stringify than full observation/snapshot payloads; regressions should be treated as performance contract failures unless the API intentionally changes.
399
+ - This is a contributor regression tool, not a host-consumer or release-note requirement.
400
+
348
401
  Run from a browser host that consumes the published package:
349
402
 
350
403
  ```bash
@@ -0,0 +1,64 @@
1
+ # Theming
2
+
3
+ PaddockJS exposes a public theme API so host websites can sync simulator colors with site-level light/dark modes without copying private CSS variable names.
4
+
5
+ ## Mount-Time Theme
6
+
7
+ ```js
8
+ await mountF1Simulator(root, {
9
+ drivers,
10
+ entries,
11
+ theme: {
12
+ mode: 'system',
13
+ tokens: {
14
+ primary: { light: '#008c55', dark: '#00ff84' },
15
+ pitLane: '#7c3aed',
16
+ },
17
+ },
18
+ });
19
+ ```
20
+
21
+ Mount-time `theme` sets defaults for a new simulator.
22
+
23
+ ## Runtime Theme Changes
24
+
25
+ Use controller methods for site toggles:
26
+
27
+ ```js
28
+ simulator.setThemeMode('dark');
29
+
30
+ simulator.setTheme({
31
+ mode: 'light',
32
+ tokens: { primary: '#008c55' },
33
+ });
34
+
35
+ const stopThemeSync = simulator.syncThemeFrom(document.documentElement, {
36
+ attribute: 'data-theme',
37
+ map: { light: 'light', dark: 'dark' },
38
+ });
39
+ ```
40
+
41
+ Do not call `restart({ theme })` only to change presentation. Runtime theme APIs update package CSS variables while preserving race state, selected driver, active penalties, speed, camera state, and pit intent state.
42
+
43
+ ## Public Helpers
44
+
45
+ ```js
46
+ import {
47
+ DEFAULT_PADDOCK_THEME,
48
+ PADDOCK_THEME_CSS_VARIABLES,
49
+ PADDOCK_THEME_TOKEN_KEYS,
50
+ applyPaddockTheme,
51
+ resolvePaddockTheme,
52
+ } from '@inventure71/paddockjs';
53
+
54
+ applyPaddockTheme(previewRoot, resolvePaddockTheme({
55
+ ...DEFAULT_PADDOCK_THEME,
56
+ mode: 'light',
57
+ }));
58
+ ```
59
+
60
+ Use these helpers when a host needs a preview, custom theme editor, or site-level sync. Do not mirror `--paddock-color-*` names manually in host code.
61
+
62
+ ## Fonts
63
+
64
+ The package stylesheet uses system font stacks and does not load remote fonts. Hosts that want branded typography should load fonts in the host app and then theme the simulator through public tokens/CSS variables.
package/docs/training.md CHANGED
@@ -94,7 +94,7 @@ Use a user-owned controller for browser playback and JavaScript training-style l
94
94
  ```js
95
95
  import {
96
96
  createPaddockDriverControllerLoop,
97
- } from '@inventure71/paddockjs';
97
+ } from '@inventure71/paddockjs/environment';
98
98
 
99
99
  const controller = {
100
100
  async init(ctx) {
@@ -182,13 +182,15 @@ const env = createPaddockEnvironment({
182
182
 
183
183
  Only requested ray channels are computed. The default compact config still computes the existing road-edge and car readings, so existing policy vectors keep their current shape unless the profile or sensor config opts into richer fields. Barrier walls are not a ray channel; they are visible track geometry and a terminal destruction condition in both physics modes. Active ray objects, vectors, schemas, and visualizations must not expose barrier ray fields.
184
184
 
185
- `precision: 'driver'` is the default and the recommended model-facing ray contract. It returns the normal sampled driver-sensor distances without extra refinement. `precision: 'debug'` exists only for clearly labeled diagnostics with additional edge refinement. If the Policy Runner or expert visualization is showing model senses, it must render the active observation values exactly and must not replace them with debug-precision readings.
185
+ `precision: 'driver'` is the default and the recommended model-facing ray contract. Safe main-track road-edge, kerb, and illegal-surface rays use direct indexed boundary intersections against the same surface bands used by wheel surfaces and rendering. Normal driver rays validate direct candidates against driver-precision track state; the `batch-training` profile keeps the accelerated direct indexed contract for safe main-track rays. Surface-only batch-training rays also reuse same-pose `progress`/`signedOffset` origin-state synthesis plus shared surface-hit and track-band boundary scratch, so kerb/illegal-surface vector batches avoid nearest-track origin classification and per-ray hit-object replacement. Recovery rays can also use the direct path when the indexed boundary is validated against driver-precision track state, including barrier-to-runoff illegal-surface recovery and off-track road/kerb re-entry. Low-curvature recovery and validation samples may reuse the origin segment to stay off generic nearest-track classification, while curved ambiguous recovery, pit-connector, and pit-lane-origin rays stay inside the ray-band tracer and use one bounded sampled validation pass for all requested road/surface channels instead of falling back to separate legacy road and surface scans. `precision: 'debug'` uses the same tracer-owned path with additional sampled refinement for clearly labeled diagnostics; only missing-index cases use the older sampled fallback path. If the Policy Runner or expert visualization is showing model senses, it must render the active observation values exactly and must not replace them with debug-precision readings.
186
+
187
+ Scratch-backed driver ray batches keep requested-channel dispatch in reusable scalar channel flags rather than rebuilding channel `Set` containers per ray. The indexed boundary traversal also reuses one query-index sample point across ray-grid steps instead of allocating temporary points. The runtime benchmark gate enforces zero channel-set allocations, stable channel-flag containers, and sample-point reuse for the common direct ray path.
186
188
 
187
189
  When a full observation includes both `object` and `vector`, Policy Runner model inputs that need ray geometry use the observation object's actual `angleDegrees` and `lengthMeters`. Compact vector schemas intentionally carry ray channel values, not out-of-band ray geometry, so consumers with per-driver sensor overrides must read the active object observation or the exact per-driver spec instead of reconstructing layout from a default preset.
188
190
 
189
191
  The model-facing sense proof boundary is documented in [Model Sense Contract](sense_contract.md). The executable contract test compares object observations against independent simulator-snapshot oracles, decodes vector entries by schema name, and checks compact vector-only output against full output for the same deterministic state. New model-facing sense fields must extend that oracle instead of relying only on snapshot or shape tests.
190
192
 
191
- Track-position, wheel-surface, pit-lane, and ray queries use the internal startup-built track query index. Browser/expert mounts, headless environments, direct race-simulation construction, compact vector-only training loops, and `batch-training` ray runs all use that canonical indexed path. This does not change the observation shape. Ray `precision: 'driver'` uses indexed projection plus driver-precision validation for road-edge, kerb, and illegal-surface distances, preserving zero-distance origin surface hits and avoiding debug-only refinement in policy inputs. Debug precision remains available only for explicitly labeled diagnostics.
193
+ Track-position, wheel-surface, pit-lane, and ray queries use the internal startup-built track query index. Browser/expert mounts, headless environments, direct race-simulation construction, compact vector-only training loops, and `batch-training` ray runs all use that canonical indexed path. Indexed pit-road state resolution reuses route candidate buckets, one polyline projection result, and index-owned cumulative route distance tables for entry, fast-lane, working-lane, and exit classification; connector-adjacent direct pit-entry/pit-exit shortcut checks reuse that same projection target and route metadata. Endpoint-progress shortcut checks use precomputed route endpoint segment windows, while mid-connector points use bounded indexed route candidates without becoming full pit-lane queries. The runtime track-query benchmark explicitly probes those pit-road routes and requires direct connector scratch reuse, positive endpoint-window connector projections, zero connector full-route projection scans, grid hits, scratch reuse, precomputed route distances, zero cumulative-distance rebuilds, zero road-grid misses, and zero pit fallbacks. This does not change the observation shape. Ray `precision: 'driver'` uses indexed projection plus driver-precision validation for road-edge, kerb, and illegal-surface distances, preserving zero-distance origin surface hits and avoiding debug-only refinement in policy inputs. The internal ray-band tracer reports whether a ray used the pure `direct` path or bounded `sampled` recovery so benchmarks can catch regressions without changing public ray objects or vectors. Debug precision remains available only for explicitly labeled diagnostics.
192
194
 
193
195
  `resetDrivers()` is part of episode control, not policy action. After reset placement, the environment immediately reclassifies the selected cars against the same runoff/barrier rules used during stepping before building observations. Recovery starts inside the legal recovery band remain normal physical starts. Placements that are already inside the barrier destruction boundary return a terminal per-driver episode state and miss-valued rays, so a batch loop can assign its own negative reward and reset that driver without paying for far-out ray geometry. In multi-driver batches, top-level `done` is true once every reported controlled driver is terminal, including mixed states where one driver is terminated and another is max-step truncated; per-driver `info.drivers[driverId]` keeps the distinct reason.
194
196
 
@@ -288,6 +290,8 @@ Use that override deliberately. The default no-collision training profile is sen
288
290
 
289
291
  Compact vector mode is intended for high-throughput loops. `env.getObservationSpec()` remains the canonical schema source, so external code can request `output: 'vector'` and `includeSchema: false` without serializing object observations and schema data on every step. If `sensorsByDriver` changes ray or nearby-car shape for specific drivers, read `observationSpec.perDriver[driverId].vector.schema` for that driver's exact compact schema. JavaScript training loops can also request `vectorType: 'float32'` for typed numeric buffers. Keep the default array output for JSON-only bridges unless the bridge explicitly packs typed arrays.
290
292
 
293
+ The HTTP Policy Runner server path uses this compact convention by default. `/policy/reset` sends `protocolVersion: 2` plus static metadata, `previousActionFields`, and `metricFields` once, and `/policy/decide-batch` sends only `driverIds` plus aligned `vectors`, `previousActions`, `metrics`, and `events` arrays. Servers should read model inputs from `body.vectors[index]` where `body.driverIds[index]` is the driver being evaluated; `previousActions[index]` is a `[steering, throttle, brake]` tuple for the same driver, and `metrics[index]` follows the reset-time `metricFields` order. Rich observation objects and schemas are no longer repeated on every decision. This is a breaking transport contract for policy-server integrations. There is no rich per-decision fallback in Policy Runner server mode; use object/full observation output only in local JavaScript, headless, debug, or explicit state/export flows.
294
+
291
295
  Use `result.stateOutput` to avoid returning more state than the loop needs:
292
296
 
293
297
  ```js
@@ -307,17 +311,21 @@ const env = createPaddockEnvironment({
307
311
 
308
312
  Advanced-mode velocity senses are authoritative only when the simulator owns a real world velocity. Arcade snapshots intentionally expose `velocityX/Y` as `null`. When advanced-mode cars are terminal or held by race control, grid logic, or pit queue logic, the environment freezes scalar speed and `velocityX/Y` together at zero instead of carrying old motion into observations or recorded transitions.
309
313
 
310
- On the local 20-car batch-training benchmark used for the original index work (`physicsMode: 'advanced'`, `frameSkip: 4`, `physical-driver`, `driver-front-heavy` rays), the measured environment action cost after compact output and indexed track queries was approximately:
314
+ On the local 3.0.0-to-4.0.0 comparison runs (`physicsMode: 'advanced'`, `physical-driver`, `driver-front-heavy` rays, procedural worst-seed checks, and vector observations), the current engine-style paths measured as a net runtime improvement in real environment stepping:
311
315
 
312
316
  ```txt
313
- nearest track states 1.52x faster than the pre-indexed path
314
- pit lane states 2.02x faster than the pre-indexed path
315
- sampled high-curvature off-track rays 14.81x faster than the pre-indexed path
316
- wheel surface near pit connector 1.12x faster than the pre-indexed path
317
- 20-car batch recovery environment 1.03x faster than the pre-indexed path
317
+ 20-car race step 4.67x faster than 3.0.0
318
+ normal env, seed 104729 4.05x faster than 3.0.0
319
+ normal env, seed 4101 1.45x faster than 3.0.0
320
+ normal env, seed 47 5.07x faster than 3.0.0
321
+ batch-training env, worst seeds 1.32x-1.39x faster than 3.0.0
322
+ wheel surface near pit connector 89.27x faster than 3.0.0
323
+ snapshot JSON, 20 cars 4.21x faster than 3.0.0
318
324
  ```
319
325
 
320
- These numbers are hardware- and track-dependent, but the relative result is the useful guidance: compact state/observation output reduces serialization/allocation overhead, and indexed track queries prevent off-track recovery rays from becoming the dominant cost center.
326
+ Those numbers are hardware- and track-dependent, but the relative result is the useful guidance: compact state/observation output reduces serialization/allocation overhead, indexed track queries prevent off-track recovery rays from becoming the dominant cost center in real environment loops, and direct indexed ray-band intersections keep common driver-precision road/surface rays off the expensive sampled nearest-query path. The high-volume seed sweep covered 69 procedural seeds, 447,120 ray builds, 3,129,840 individual rays, 149,040 wheel-surface classifications, and 1,380 environment steps with zero invalid ray payloads, zero invalid environment observations, zero invalid wheel classifications, and zero nearest/pit fallbacks.
327
+
328
+ There is one deliberate microbenchmark caveat: adversarial standalone `buildRaySensors()` sweeps that force normal-profile rays from kerb, gravel, and runoff poses on complex generated tracks can be slower than 3.0.0 because the current path performs more bounded indexed exact validation to preserve driver-precision surface semantics. On the worst checked seeds, real environment stepping still measured faster than 3.0.0. Treat the standalone ray torture case as a precision/diagnostic stress test, not the expected training-loop cost.
321
329
 
322
330
  ## Scenario Reset Control
323
331
 
@@ -416,10 +424,8 @@ Deterministic evaluation helpers report simulator quality metrics such as distan
416
424
  ## Visual Playback Loop
417
425
 
418
426
  ```js
419
- import {
420
- createPaddockDriverControllerLoop,
421
- mountF1Simulator,
422
- } from '@inventure71/paddockjs';
427
+ import { createPaddockDriverControllerLoop } from '@inventure71/paddockjs/environment';
428
+ import { mountF1Simulator } from '@inventure71/paddockjs';
423
429
 
424
430
  const simulator = await mountF1Simulator(root, {
425
431
  drivers,