@inventure71/paddockjs 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/INSTALL_AND_UPDATE.md +10 -1
  3. package/README.md +65 -15
  4. package/docs/architecture.md +17 -13
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/concepts.md +2 -2
  7. package/docs/custom_model_controller.md +2 -2
  8. package/docs/data_contract.md +96 -21
  9. package/docs/index.md +2 -1
  10. package/docs/learnings.md +6 -6
  11. package/docs/rules.md +7 -7
  12. package/docs/sense_contract.md +1 -1
  13. package/docs/system_specs.md +25 -18
  14. package/docs/training.md +13 -12
  15. package/package.json +1 -1
  16. package/src/api/PaddockSimulatorController.js +74 -4
  17. package/src/app/F1SimulatorApp.js +191 -28
  18. package/src/app/banners/raceDataBannerController.js +71 -36
  19. package/src/app/banners/stewardMessageController.js +5 -4
  20. package/src/app/camera/cameraConstants.js +2 -0
  21. package/src/app/camera/cameraController.js +86 -29
  22. package/src/app/camera/cameraGeometry.js +12 -1
  23. package/src/app/domBindings.js +89 -9
  24. package/src/app/layoutScheduler.js +27 -0
  25. package/src/app/layoutSupport.js +165 -0
  26. package/src/app/raceOverlayClearanceSupport.js +158 -0
  27. package/src/app/readouts/carOverviewRenderer.js +16 -8
  28. package/src/app/readouts/controlStateRenderer.js +1 -1
  29. package/src/app/readouts/raceStatusRenderer.js +30 -21
  30. package/src/app/readouts/readoutFormatters.js +2 -0
  31. package/src/app/readouts/telemetryRenderer.js +17 -8
  32. package/src/app/readouts/timingTowerRenderer.js +62 -10
  33. package/src/app/runtime/runtimeVisibility.js +66 -5
  34. package/src/config/cssValues.js +16 -0
  35. package/src/config/defaultOptions.js +51 -38
  36. package/src/config/themeOptions.js +825 -0
  37. package/src/config/timingGapMode.js +13 -0
  38. package/src/config/urlOptions.js +25 -0
  39. package/src/data/championship.js +14 -9
  40. package/src/environment/controllerLoop.js +17 -3
  41. package/src/environment/index.d.ts +1 -2
  42. package/src/environment/options.js +1 -1
  43. package/src/environment/runtime.js +3 -11
  44. package/src/environment/scenarios.js +1 -1
  45. package/src/environment/sensors/surfaceRays.js +41 -33
  46. package/src/environment/sensors/trackRays.js +44 -49
  47. package/src/index.d.ts +110 -9
  48. package/src/index.js +25 -1
  49. package/src/simulation/driver/driverController.js +1 -1
  50. package/src/simulation/driver/edgeRecovery.js +1 -1
  51. package/src/simulation/driver/racingControls.js +2 -2
  52. package/src/simulation/driver/rejoinControls.js +2 -2
  53. package/src/simulation/pit/pitKinematics.js +2 -2
  54. package/src/simulation/pit/pitRouting.js +2 -4
  55. package/src/simulation/race/raceSetup.js +2 -6
  56. package/src/simulation/race/raceStep.js +1 -1
  57. package/src/simulation/race/safetyCar.js +1 -1
  58. package/src/simulation/race/stalledDnf.js +1 -1
  59. package/src/simulation/timing/lapTelemetry.js +28 -9
  60. package/src/simulation/track/pitLaneLayout.js +7 -3
  61. package/src/simulation/track/spatialQueries.js +5 -28
  62. package/src/simulation/track/trackQueryIndex.js +180 -135
  63. package/src/simulation/track/trackStatePolicy.js +0 -1
  64. package/src/simulation/vehicle/contactResolution.js +3 -3
  65. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  66. package/src/simulation/vehicle/vehiclePhysics.js +3 -3
  67. package/src/simulation/vehicle/vehicleSnapshots.js +12 -1
  68. package/src/simulation/vehicle/vehicleState.js +1 -1
  69. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  70. package/src/styles.css +1185 -136
  71. package/src/ui/bannerTemplates.js +38 -28
  72. package/src/ui/cameraControlsTemplate.js +16 -5
  73. package/src/ui/carOverviewTemplate.js +12 -5
  74. package/src/ui/raceCanvasTemplate.js +31 -8
  75. package/src/ui/raceControlsTemplate.js +27 -10
  76. package/src/ui/shellTemplate.js +3 -1
  77. package/src/ui/telemetryDrawerTemplate.js +23 -10
  78. package/src/ui/telemetryTemplates.js +58 -32
  79. package/src/ui/templateUtils.js +48 -0
  80. package/src/ui/timingTowerTemplate.js +29 -12
@@ -60,10 +60,10 @@ Environment subpath exports:
60
60
  | `createEnvironmentWorkerProtocol`, `handleEnvironmentMessage` | JSON-serializable worker bridge helpers |
61
61
 
62
62
  `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 can use an internal non-enumerable track query index built with the track model. Browser/expert mounts, headless environments, and direct race-simulation construction now default to the indexed path (opt-out with `trackQueryIndex: false`); compact training-style environment runs (including `batch-training` rays with full state output) still auto-enable it. This changes runtime cost, not public observation shapes.
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.
64
64
  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
65
  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
- `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. It does not load checkpoints, run neural networks, parse model formats, compute rewards, or mutate vehicle state.
66
+ `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.
67
67
 
68
68
  All-in-one mount call:
69
69
 
@@ -75,7 +75,6 @@ const simulator = await mountF1Simulator(root, {
75
75
  seed,
76
76
  trackSeed,
77
77
  trackGeneration,
78
- trackQueryIndex,
79
78
  warmup,
80
79
  totalLaps,
81
80
  physicsMode,
@@ -104,6 +103,8 @@ const simulator = await mountF1Simulator(root, {
104
103
  });
105
104
  ```
106
105
 
106
+ The all-in-one shell normalizes `backLinkHref` before rendering it. Package-owned race controls accept relative URLs, hash URLs, and absolute `http:` / `https:` URLs; unsafe or malformed values fall back to the default `projects.html` link instead of being rendered into the anchor. Runtime readouts also validate snapshot-derived color values before writing CSS custom properties, and package-owned asset URLs are escaped before becoming CSS `url(...)` values.
107
+
107
108
  Composable mount call:
108
109
 
109
110
  ```js
@@ -114,7 +115,6 @@ const simulator = createPaddockSimulator({
114
115
  seed,
115
116
  trackSeed,
116
117
  trackGeneration,
117
- trackQueryIndex,
118
118
  warmup,
119
119
  totalLaps,
120
120
  physicsMode,
@@ -238,31 +238,36 @@ Returned controller:
238
238
  - Composable mounting can embed the timing tower directly inside the race canvas with `mountRaceCanvas(root, { includeTimingTower: true })`.
239
239
  - The race canvas is required before `start()` because PixiJS needs a canvas host.
240
240
  - Timing tower, telemetry, controls, and race-data panels are optional from a runtime safety perspective; omitted panels simply do not render their readouts.
241
- - Timing tower entries are fixed 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.
242
- - Timing tower entries display team icons. The tower can switch at runtime between interval-to-car-ahead timing and direct gap-to-leader timing. Seconds gaps are calculated from hidden timing-line crossings, and whole-lap deficits display as `+N`.
241
+ - 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
+ - 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
243
  - 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
244
  - 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
245
  - Entries can include optional `team` metadata. Team color defaults to car color when omitted.
246
246
  - Mounted package surfaces show a package-owned red start-light loading overlay until `start()` finishes PixiJS, asset, control, and initial readout initialization.
247
247
  - `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
- - `theme` is the public sizing/color contract. It maps to package CSS variables for `accentColor`, `greenColor`, `yellowColor`, `timingTowerMaxWidth`, and `raceViewMinHeight`.
249
- - `initialCameraMode` accepts `'overview'`, `'leader'`, `'selected'`, `'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 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, and pit, but cannot zoom farther out than the active track frame.
250
- - 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.
248
+ - `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.
249
+ - `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
+ - 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
251
  - 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.
252
- - `mountRaceTelemetryDrawer()` creates a package-owned race workbench: external top controls, race canvas, embedded timing tower, lower-third banner, top steward message, safety-car control, and a right-side telemetry drawer. The top row holds camera controls, banner mute, safety car, and telemetry toggle outside the canvas. Pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact project telemetry detail. The drawer opens smoothly, takes width from the race view instead of overlaying it, is inert/hidden to interaction when closed, and embeds the same `telemetry-stack` component used by standalone telemetry panels.
252
+ - `mountRaceTelemetryDrawer()` creates a package-owned race workbench: external top controls, race canvas, embedded timing tower, lower-third banner, top steward message, safety-car control, and a right-side telemetry drawer. The top row holds camera controls, banner mute, safety car, and telemetry toggle outside the canvas. On narrow responsive containers, that toolbar stacks into readable full-width rows and keeps safety car plus telemetry as a two-column action row instead of squeezing them beside the camera controls. Pass `{ raceDataTelemetryDetail: true }` when the drawer lower-third should include compact project telemetry detail. The drawer opens smoothly, takes width from the race view instead of overlaying it, is inert/hidden to interaction when closed, and embeds the same `telemetry-stack` component used by standalone telemetry panels.
253
253
  - The FPS readout can be shown or hidden with `ui.showFps`.
254
254
  - `ui.layoutPreset: 'left-tower-overlay'` is a package-owned preset that creates a left broadcast gutter inside the race view, places the timing tower there at the same width as the default timing-board column, frames the PixiJS camera around the remaining usable race area, and keeps camera controls and start lights out of the tower area. In the combined shell, project and radio lower-thirds stay inside the race window while being allowed to cover the timing sidebar.
255
255
  - `ui.raceDataBanners.initial` controls the starting lower-third (`'project'`, `'radio'`, or `'hidden'`), and `ui.raceDataBanners.enabled` controls which lower-third types can appear. The runtime banner mute state temporarily disables project/radio lower-thirds without changing these configured allow-lists.
256
256
  - Project and radio lower-third pills include a package-owned top-right close button that dismisses the current pill early. The next pill appears only through normal driver selection or radio scheduling.
257
+ - When a composable host mounts more than one package-owned race-data panel, such as an embedded race-canvas lower-third plus `mountRaceDataPanel()`, the panels share the same runtime project/radio/hide state and each panel's open/dismiss controls remain wired.
257
258
  - `ui.raceDataBannerSize` controls lower-third sizing: `'custom'` keeps the default CSS-variable-driven banner size for host tuning, while `'auto'` uses the race space to the right of the timing board when wide enough and overlaps the timing board only when space is constrained.
258
259
  - `ui.raceDataTelemetryDetail` adds compact S1/S2/S3 sector detail to the project lower-third while keeping radio mode unchanged.
259
260
  - `ui.timingTowerVerticalFit` controls vertical tower behavior in the combined overlay preset: `'expand-race-view'` grows the race window to fit the tower, while `'scroll'` crops the tower area and scrolls timing rows inside it. The same values are accepted as `mountRaceCanvas()` options when `includeTimingTower` embeds the tower in the race canvas.
260
- - Hosts may scale the whole mounted simulator through the container. The horizontal proportions inside package-owned presets are not public API and should not be configurable through raw width or ratio options. The camera reads the current canvas dimensions so wider or taller host windows reveal more of the race view without needing host-owned camera math, and the renderer keeps grass coverage beyond the simulated world so deep zoom-out does not show host background. The timing tower has a package-owned max width because overly wide timing boards degrade readability; standalone hosts can constrain vertical height through the mount container and let the timing entries scroll internally. Mobile and narrow embeds are handled by package CSS: timing boards stack full-width when they no longer work as side gutters, camera controls stay in external control rows when mounted, and full-width timing boards do not reserve horizontal camera gutter space.
261
+ - `ui.timingGapMode` controls whether the timing tower starts in `'interval'` or `'leader'` mode. `ui.timingGapModeToggle` defaults to `true`; setting it to `false` hides the manual header toggle while preserving runtime control through the mounted simulator/controller methods. Hosts that need a different control placement should hide the package toggle and call those methods from host-owned UI instead of restyling package internals.
262
+ - Hosts may scale the whole mounted simulator through the container. The horizontal proportions inside package-owned presets are not public API and should not be configurable through raw width or ratio options. The supported inline-size envelope starts at `320px` for the package-owned mount root or individual composable surface. Hosts should provide responsive outer containers with `min-width: 0` and avoid targeting package internals; PaddockJS owns timing-board width, camera safe-area math, lower-third containment, drawer sizing, and internal density tiers. The camera reads the current canvas dimensions so wider or taller host windows reveal more of the race view without needing host-owned camera math, and the renderer keeps grass coverage beyond the simulated world so deep zoom-out does not show host background. The timing tower has a package-owned max width because overly wide timing boards degrade readability; standalone hosts can constrain vertical height through the mount container and let the timing entries scroll internally. Mobile and narrow embeds are handled by package CSS plus a package-owned resize observer: embedded timing towers become left reveal panels when they no longer work as side gutters, camera controls stay in external control rows when mounted, narrow telemetry drawers reveal over the race view instead of forcing permanent canvas shrinkage, and closed reveal panels stay inert/hidden while the renderer and camera resize continuously without simulator remounts. A narrow lower-third may cover empty timing-tower chrome, but if the measured lower-third would cover any timing entry, PaddockJS adds measured lower-third clearance to the race view's minimum height; when timing entries remain clear, the height remains governed by the timing tower/race-view minimum. Template responsive narrow variants are enabled by default through `ui.responsiveNarrowLayout`; hosts can set `ui.responsiveNarrowLayout: false`, or pass `responsiveNarrowLayout: false` to composable race-canvas/drawer mounts, to opt out.
263
+ - Package layout density tiers are container-owned: `320px`-`519px` is mobile density, `520px`-`759px` is narrow tablet density, `760px`-`1119px` is tablet density, and `1120px` and wider is desktop density. Component behavior follows the actual package container width, not only the browser viewport, so embedded timing towers and composable pieces remain safe inside split tablet layouts.
264
+ - If the shell/mount root is narrower than `320px`, or if an individual package component is constrained below its component-specific inline/block minimum, the component sets `data-paddock-size-unsupported="true"` and displays a package-owned accessible unsupported-size placeholder instead of broken UI. The broadcast timing tower is one intentional exception to a blanket `320px` subcomponent rule: it remains valid at its package-owned narrow timing-board width. The placeholder uses `role="status"` and `aria-live="polite"`. Hosts should fix the outer container size rather than hiding package content or patching package internals.
265
+ - Package controls, drawer toggles, lower-third actions, and dismiss buttons maintain practical `44px` touch targets, shared visible focus states, and `touch-action: manipulation`. Timing rows are compact selectable broadcast entries controlled by `ui.timingEntryVerticalPadding`. Touch driving or virtual joystick controls are intentionally out of scope.
261
266
  - The host does not need to provide simulator assets.
262
267
  - The host passes data, not internal DOM.
263
268
  - Host driver IDs and entry `driverId` values must be unique. Entries may omit `driverNumber`; provided numbers must be unique.
264
269
  - `totalLaps` is normalized to a finite positive integer before simulation so invalid input cannot produce zero-lap, negative-lap, or non-finite snapshots.
265
- - `physicsMode` accepts `'arcade'` and `'simulator'`. The default is `'arcade'` to preserve existing hosts. `'simulator'` is opt-in and keeps cars controlled only through steering, throttle, brake, and pit intent while enabling traction-budget limits, steering scrub, velocity-heading slip, surface-specific grip/drag, and simulator telemetry. Snapshots expose `physicsMode` plus per-car `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, `stabilityState`, and the latest `appliedControls`.
270
+ - `physicsMode` accepts `'arcade'` and `'advanced'`. The default is `'arcade'` to preserve existing hosts. `'advanced'` is opt-in and keeps cars controlled only through steering, throttle, brake, and pit intent while enabling traction-budget limits, steering scrub, velocity-heading slip, surface-specific grip/drag, and advanced telemetry. The old `'simulator'` mode name is removed and falls back to `'arcade'` like any other invalid value. Snapshots expose `physicsMode` plus per-car `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, `stabilityState`, and the latest `appliedControls`.
266
271
  - `restart(nextOptions)` can change race data and deterministic seeds such as `trackSeed`, but it does not support changing asset URLs. Asset changes require `destroy()` and a fresh mount because PixiJS texture loading is an initialization boundary.
267
272
  - `onDriverOpen(driver)` is the navigation boundary.
268
273
  - Lifecycle callbacks are optional: `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`. Lifecycle callback failures are routed to `onError` when possible and must not stop the simulator loop. `onDriverOpen(driver)` is host-owned navigation code and is not wrapped as a lifecycle callback.
@@ -276,15 +281,15 @@ Returned controller:
276
281
  - 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.
277
282
  - 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.
278
283
  - The renderer should target a paced 60 FPS simulation/render loop.
279
- - The render loop should pause while the race canvas is offscreen or the document is hidden, then resume without catching up the elapsed hidden time. Layout measurements needed for overlay camera safe areas should be cached between resize/layout invalidations. Runtime DOM updates should skip unchanged text/markup so visible embeds do not rewrite stable readouts every frame.
284
+ - The render loop should pause while the race canvas is offscreen or the document is hidden, then resume without catching up the elapsed hidden time. Runtime visibility must not rely on a single observer state: it should resync from canvas geometry after observer, scroll, resize, focus, and page-show signals, and ticker sync should reconcile the intended runtime state with PixiJS' actual started/stopped state. Layout measurements needed for overlay camera safe areas should be cached between resize/layout invalidations. Runtime DOM updates should skip unchanged text/markup so visible embeds do not rewrite stable readouts every frame.
280
285
  - Restart and rerender paths must destroy replaced PixiJS display children while preserving shared loaded textures.
281
286
  - Restart supports race/data/seed changes but does not support asset URL changes or browser expert mode changes. Texture loading and ticker ownership are mount-time boundaries; hosts must destroy and mount again to change either boundary.
282
287
  - 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.
283
288
  - 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.
284
289
  - 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.
285
- - 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. For normal driver interactions, `precision: 'driver'` road-edge, kerb, and illegal-surface channels keep the sampled model-facing distances whether the track query index is enabled or disabled. The `batch-training` participant profile keeps its indexed recovery-ray path as its high-throughput training contract; other indexed ray-boundary shortcuts are reserved for non-driver/debug diagnostics. 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.
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.
286
291
  - 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.
287
- - Browser expert mode is opt-in with `expert: { enabled: true, controlledDrivers, frameSkip }`. When enabled, the returned controller exposes `expert.reset()`, `expert.step(actions)`, `expert.getObservation()`, and `expert.getState()`.
292
+ - 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()`.
288
293
  - 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? }`.
289
294
  - Browser expert mode wraps the same `RaceSimulation` instance that the visual canvas renders. It must not create a parallel simulation for the same mount.
290
295
  - Browser expert mode disables automatic ticker-driven simulation advancement. The visual canvas updates only after explicit expert `reset()` or `step(actions)` calls.
@@ -298,8 +303,8 @@ Returned controller:
298
303
  - Race canvas rendered with PixiJS.
299
304
  - Procedural track rendering with asphalt texture and DRS overlays.
300
305
  - Driver selection from cars and timing tower rows.
301
- - Camera modes: overview, leader, selected, show all, and pit.
302
- Overview frames the active generated track bounds with package-owned padding and pit-lane extent; show all dynamically fits the active pack; pit frames the operational pit-lane work area when the track has one.
306
+ - Camera modes: overview, leader, selected, driver, show all, and pit.
307
+ Overview frames the active generated track bounds with package-owned padding and pit-lane extent; driver rotates around the selected car for user-driving views; show all dynamically fits the active pack; pit frames the operational pit-lane work area when the track has one.
303
308
  - Zoom controls.
304
309
  - FPS readout.
305
310
  - Start lights.
@@ -313,6 +318,8 @@ Returned controller:
313
318
  - Race-finish winner banner and final top-three classification.
314
319
  - `Open project` button driven by `onDriverOpen(driver)`.
315
320
 
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.
322
+
316
323
  ## Runtime Requirements
317
324
 
318
325
  - Browser environment with DOM APIs.
@@ -336,7 +343,7 @@ Expected:
336
343
  - `npm pack --dry-run` succeeds and includes source files plus bundled assets.
337
344
  - A packed tarball installs and builds inside a fresh temporary Vite consumer app.
338
345
  - The tracked showcase host builds.
339
- - The quick Chromium browser smoke verifies showcase canvas rendering, overflow constraints, and one public API action. The release browser smoke verifies the desktop/mobile matrix, package-panel overflow constraints, public API buttons, and visual policy-runner stepping across generation and race configurations.
346
+ - 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.
340
347
 
341
348
  Run from a browser host that consumes the published package:
342
349
 
package/docs/training.md CHANGED
@@ -46,6 +46,7 @@ The current expert API is a JavaScript environment contract. It supports:
46
46
  - reset-only scenario placement through `scenario.preset`, `scenario.placements`, `scenario.traffic`, and episode-boundary `resetDrivers(placements)`
47
47
  - multi-car interaction profiles through `participantInteractions`, including no-collision training cars that are hidden from other cars' ray and nearby-car sensors by default
48
48
  - per-driver episode state and neutral step metrics for batched training loops
49
+ - `episode.maxSteps` as a controlled-driver episode safety horizon; the default is high for browser/expert usability, so training and evaluation loops should set an explicit smaller value
49
50
  - neutral rollout recording through `createRolloutRecorder()`
50
51
  - deterministic evaluation metrics through `runEnvironmentEvaluation()` and `createEvaluationTracker()`
51
52
  - a JSON-serializable worker protocol wrapper through `createEnvironmentWorkerProtocol()`
@@ -155,15 +156,15 @@ built-in driver applied. This is intended for audits or clean local imitation
155
156
  datasets; it does not blend built-in AI with a model during normal controlled
156
157
  stepping.
157
158
 
158
- For model training, prefer `physicsMode: 'simulator'` so the policy learns against the same grip, yaw-rate, contact-patch, kerb, and runoff behavior exposed by those senses. Arcade physics can still be useful as a debugging baseline, but it should not be treated as the main training target for realistic driver policies.
159
+ For model training, prefer `physicsMode: 'advanced'` so the policy learns against the same grip, yaw-rate, contact-patch, kerb, and runoff behavior exposed by those senses. Arcade physics can still be useful as a debugging baseline, but it should not be treated as the main training target for realistic driver policies.
159
160
 
160
- Surface-aware rays are opt-in because they are more expensive than the default road-edge/car channels. The environment normalizes both the legacy compact ray shape and the newer per-ray layout:
161
+ Surface-aware rays are opt-in because they are more expensive than the default road-edge/car channels. The environment normalizes both the compact ray shape and the newer per-ray layout:
161
162
 
162
163
  ```js
163
164
  const env = createPaddockEnvironment({
164
165
  drivers,
165
166
  controlledDrivers: ['budget'],
166
- physicsMode: 'simulator',
167
+ physicsMode: 'advanced',
167
168
  observation: { profile: 'physical-driver' },
168
169
  sensors: {
169
170
  rays: {
@@ -187,7 +188,7 @@ When a full observation includes both `object` and `vector`, Policy Runner model
187
188
 
188
189
  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.
189
190
 
190
- Track-position, wheel-surface, pit-lane, and ray fallback queries can be backed by an internal startup-built track query index. Browser/expert mounts, headless environments, and direct race-simulation construction now default to indexed queries and can opt out with `trackQueryIndex: false` for comparison runs. Compact vector-only, schema-free, no-state environment runs enable that index on the environment-owned simulation, and `batch-training` ray runs keep it enabled even when full state output is returned for inspection. Internal benchmarks can explicitly compare indexed and legacy-disabled modes. This does not change the observation shape. For normal driver interactions, ray `precision: 'driver'` keeps sampled road-edge, kerb, and illegal-surface distances with or without the index, preserving zero-distance origin surface hits and avoiding debug-only precision in policy inputs. The `batch-training` participant profile keeps its indexed recovery-ray path as a separate high-throughput training contract with characterization coverage. Indexed ray-boundary shortcuts remain available to non-driver/debug precision paths.
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.
191
192
 
192
193
  `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.
193
194
 
@@ -304,16 +305,16 @@ const env = createPaddockEnvironment({
304
305
 
305
306
  `stateOutput: 'minimal'` returns the lean public observation snapshot. `stateOutput: 'none'` returns `state: null`. The default is still `full` for existing callers. `resetDriversObservationScope: 'reset'` also scopes `info.controlledDrivers` to the drivers present in that reset result's `observation` and `metrics`; use `info.drivers` if you need all configured drivers' episode bookkeeping. When `stateOutput: 'none'`, `observation.output: 'vector'`, and `includeSchema: false`, the environment uses an internal compact training snapshot before building observations and metrics. That optimization is not exposed as policy state. Reward callbacks still receive the documented reward context on `step(actions)`, and reset/read calls remain reward-neutral.
306
307
 
307
- Simulator-mode velocity senses are authoritative only when the simulator owns a real world velocity. Arcade snapshots intentionally expose `velocityX/Y` as `null`. When simulator-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.
308
+ 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.
308
309
 
309
- On the local 20-car batch-training benchmark used for this package work (`physicsMode: 'simulator'`, `frameSkip: 4`, `physical-driver`, `driver-front-heavy` rays), the measured environment action cost after compact output and indexed track queries was approximately:
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:
310
311
 
311
312
  ```txt
312
- nearest track states 1.52x faster than legacy-disabled
313
- pit lane states 2.02x faster than legacy-disabled
314
- sampled high-curvature off-track rays 14.81x faster than legacy-disabled
315
- wheel surface near pit connector 1.12x faster than legacy-disabled
316
- 20-car batch recovery environment 1.03x faster than legacy-disabled
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
318
  ```
318
319
 
319
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.
@@ -404,7 +405,7 @@ recorder.recordStep(result, action, next);
404
405
 
405
406
  Each transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. If the environment has no reward callback, `reward` is still `null`; the recorder does not invent one. Recorded transitions snapshot arrays, typed vectors, actions, observations, rewards, and info at record time, so later policy-loop mutations do not rewrite previous rollout rows.
406
407
 
407
- Environment step results also include reward-neutral `metrics[driverId]` facts such as progress delta, legal progress delta, kerb use, illegal/off-track state, severe cuts, destruction state, under-30-kph state, spin/backwards/instability state, lap completion, lap time, and contact count. `spinOrBackwards` is true for large heading-error backwards cases and for active simulator instability labels such as `spin-risk`, `understeer`, `oversteer`, and `destroyed`. `contactCount` counts each normalized physical contact event once per involved driver even when the event carries both legacy `carId`/`otherCarId` fields and normalized `driverIds`. These metrics are facts for logging, evaluation, and user-defined reward functions; PaddockJS does not turn them into a built-in objective. If a car touches the rendered barrier wall's inner face in either physics mode, that driver receives `metrics[driverId].destroyed: true` and `info.drivers[driverId].endReason: 'destroyed'`; external training code should assign any super-negative crash reward itself and then call `resetDrivers()` for the next episode. If opt-in `rules.modules.stalledDnf.enabled: true` retires a controlled car for being stuck off legal surfaces, the episode terminates with `endReason: 'stalled-off-track'` while `metrics[driverId].destroyed` stays `false`.
408
+ Environment step results also include reward-neutral `metrics[driverId]` facts such as progress delta, legal progress delta, kerb use, illegal/off-track state, severe cuts, destruction state, under-30-kph state, spin/backwards/instability state, lap completion, lap time, and contact count. `spinOrBackwards` is true for large heading-error backwards cases and for active simulator instability labels such as `spin-risk`, `understeer`, `oversteer`, and `destroyed`. `contactCount` counts each normalized physical contact event once per involved driver even when the event carries older `carId`/`otherCarId` fields and normalized `driverIds`. These metrics are facts for logging, evaluation, and user-defined reward functions; PaddockJS does not turn them into a built-in objective. If a car touches the rendered barrier wall's inner face in either physics mode, that driver receives `metrics[driverId].destroyed: true` and `info.drivers[driverId].endReason: 'destroyed'`; external training code should assign any super-negative crash reward itself and then call `resetDrivers()` for the next episode. If opt-in `rules.modules.stalledDnf.enabled: true` retires a controlled car for being stuck off legal surfaces, the episode terminates with `endReason: 'stalled-off-track'` while `metrics[driverId].destroyed` stays `false`.
408
409
 
409
410
  Deterministic evaluation helpers report simulator quality metrics such as distance, off-track steps, contacts, recovery success, pass count, and first lap time when available. They use the same wheel-level legal-surface and contact-event semantics as environment metrics. `runEnvironmentEvaluation()` can accept compact base options with `result.stateOutput: 'none'`; it internally requests the minimal snapshot needed for evaluation bookkeeping. Direct `createEvaluationTracker(result)` calls require a result with `state.snapshot`, so use `stateOutput: 'minimal'` or `full` for manual tracker wiring. Evaluation helpers do not update a model or compute rewards.
410
411
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "description": "A data-driven browser F1 simulator component for host websites.",
6
6
  "license": "Apache-2.0",
@@ -1,4 +1,5 @@
1
1
  import { F1SimulatorApp } from '../app/F1SimulatorApp.js';
2
+ import { installLayoutSupport } from '../app/layoutSupport.js';
2
3
  import {
3
4
  createRaceDataPanelMarkup,
4
5
  createTelemetrySectorBannerMarkup,
@@ -18,7 +19,9 @@ import {
18
19
  createTimingTowerMarkup,
19
20
  } from '../ui/componentTemplates.js';
20
21
  import { applyPaddockThemeCssVariables, resolveF1SimulatorOptions } from '../config/defaultOptions.js';
22
+ import { formatCssUrl } from '../config/cssValues.js';
21
23
  import { mergeRestartOptions } from '../config/restartOptions.js';
24
+ import { getNextTimingGapMode, normalizeTimingGapMode } from '../config/timingGapMode.js';
22
25
 
23
26
  function assertMountTarget(root, label) {
24
27
  if (!root || typeof root !== 'object' || !('innerHTML' in root)) {
@@ -28,7 +31,7 @@ function assertMountTarget(root, label) {
28
31
 
29
32
  function setPackageCssVariables(root, assets, theme) {
30
33
  root.classList?.add?.('f1-sim-component');
31
- root.style?.setProperty?.('--broadcast-panel-surface', `url('${assets.broadcastPanel}')`);
34
+ root.style?.setProperty?.('--broadcast-panel-surface', formatCssUrl(assets.broadcastPanel));
32
35
  applyPaddockThemeCssVariables(root, theme);
33
36
  }
34
37
 
@@ -49,6 +52,12 @@ function createCompositeRoot(getRoots, getOptions) {
49
52
  querySelectorAll(selector) {
50
53
  return getRoots().flatMap((root) => [...(root.querySelectorAll?.(selector) ?? [])]);
51
54
  },
55
+ setAttribute(name, value) {
56
+ getRoots().forEach((root) => root.setAttribute?.(name, value));
57
+ },
58
+ removeAttribute(name) {
59
+ getRoots().forEach((root) => root.removeAttribute?.(name));
60
+ },
52
61
  applyCssVariables() {
53
62
  const options = getOptions();
54
63
  getRoots().forEach((root) => setPackageCssVariables(root, options.assets, options.theme));
@@ -60,6 +69,7 @@ export class PaddockSimulatorController {
60
69
  constructor(options = {}) {
61
70
  this.options = resolveF1SimulatorOptions(options);
62
71
  this.roots = new Map();
72
+ this.layoutSupportCleanups = new Map();
63
73
  this.app = null;
64
74
  this.compositeRoot = createCompositeRoot(() => [...this.roots.values()], () => this.options);
65
75
  }
@@ -71,6 +81,8 @@ export class PaddockSimulatorController {
71
81
  }
72
82
  root.innerHTML = markup;
73
83
  setPackageCssVariables(root, this.options.assets, this.options.theme);
84
+ this.layoutSupportCleanups.get(key)?.();
85
+ this.layoutSupportCleanups.set(key, installLayoutSupport(root));
74
86
  this.roots.set(key, root);
75
87
  return root;
76
88
  }
@@ -96,6 +108,7 @@ export class PaddockSimulatorController {
96
108
  includeTimingTower = false,
97
109
  includeTelemetrySectorBanner = false,
98
110
  timingTowerVerticalFit,
111
+ responsiveNarrowLayout,
99
112
  } = {}) {
100
113
  return this.mountComponent(root, 'race-canvas', createRaceCanvasMarkup({
101
114
  ...this.options,
@@ -103,6 +116,7 @@ export class PaddockSimulatorController {
103
116
  includeTimingTower,
104
117
  includeTelemetrySectorBanner,
105
118
  timingTowerVerticalFit,
119
+ responsiveNarrowLayout,
106
120
  }));
107
121
  }
108
122
 
@@ -139,7 +153,10 @@ export class PaddockSimulatorController {
139
153
  }
140
154
 
141
155
  mountRaceDataPanel(root) {
142
- return this.mountComponent(root, 'race-data-panel', createRaceDataPanelMarkup(this.options));
156
+ return this.mountComponent(root, 'race-data-panel', createRaceDataPanelMarkup({
157
+ ...this.options,
158
+ standalone: true,
159
+ }));
143
160
  }
144
161
 
145
162
  querySelector(selector) {
@@ -165,15 +182,35 @@ export class PaddockSimulatorController {
165
182
  destroy() {
166
183
  this.app?.destroy();
167
184
  this.app = null;
185
+ this.layoutSupportCleanups.forEach((cleanup) => cleanup());
186
+ this.layoutSupportCleanups.clear();
168
187
  this.roots.forEach((root) => {
169
188
  root.innerHTML = '';
170
189
  });
171
190
  this.roots.clear();
172
191
  }
173
192
 
193
+ syncOptionsFromRunningApp() {
194
+ const timingGapMode = this.app?.getTimingGapMode?.();
195
+ if (!timingGapMode) return this.options;
196
+ this.options = {
197
+ ...this.options,
198
+ ui: {
199
+ ...this.options.ui,
200
+ timingGapMode: normalizeTimingGapMode(timingGapMode),
201
+ },
202
+ };
203
+ return this.options;
204
+ }
205
+
174
206
  restart(nextOptions = {}) {
175
- const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(this.options, nextOptions));
176
- this.app?.restart(nextResolvedOptions);
207
+ const currentOptions = this.app ? this.syncOptionsFromRunningApp() : this.options;
208
+ const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(currentOptions, nextOptions));
209
+ if (this.app) {
210
+ this.app.restart(nextResolvedOptions);
211
+ this.options = nextResolvedOptions;
212
+ return;
213
+ }
177
214
  this.options = nextResolvedOptions;
178
215
  this.compositeRoot.applyCssVariables();
179
216
  }
@@ -223,6 +260,39 @@ export class PaddockSimulatorController {
223
260
  return this.app?.simulationSpeed ?? 1;
224
261
  }
225
262
 
263
+ setTimingGapMode(mode) {
264
+ const timingGapMode = this.app
265
+ ? this.app.setTimingGapMode(mode)
266
+ : normalizeTimingGapMode(mode);
267
+ this.options = {
268
+ ...this.options,
269
+ ui: {
270
+ ...this.options.ui,
271
+ timingGapMode,
272
+ },
273
+ };
274
+ return this.options.ui.timingGapMode;
275
+ }
276
+
277
+ getTimingGapMode() {
278
+ return this.app?.getTimingGapMode?.() ?? normalizeTimingGapMode(this.options.ui?.timingGapMode);
279
+ }
280
+
281
+ toggleTimingGapMode() {
282
+ if (this.app) {
283
+ const timingGapMode = this.app.toggleTimingGapMode();
284
+ this.options = {
285
+ ...this.options,
286
+ ui: {
287
+ ...this.options.ui,
288
+ timingGapMode,
289
+ },
290
+ };
291
+ return timingGapMode;
292
+ }
293
+ return this.setTimingGapMode(getNextTimingGapMode(this.options.ui?.timingGapMode));
294
+ }
295
+
226
296
  servePenalty(penaltyId) {
227
297
  return this.app?.servePenalty(penaltyId) ?? null;
228
298
  }