@inventure71/paddockjs 2.0.1 → 4.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 (136) hide show
  1. package/CHANGELOG.md +176 -1
  2. package/INSTALL_AND_UPDATE.md +24 -4
  3. package/README.md +84 -512
  4. package/docs/architecture.md +38 -24
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/composable-layouts.md +77 -0
  7. package/docs/concepts.md +2 -2
  8. package/docs/custom_model_controller.md +6 -6
  9. package/docs/data-helpers.md +50 -0
  10. package/docs/data_contract.md +104 -27
  11. package/docs/getting-started.md +75 -0
  12. package/docs/headless-environment.md +68 -0
  13. package/docs/index.md +18 -4
  14. package/docs/learnings.md +6 -6
  15. package/docs/rules.md +12 -8
  16. package/docs/sense_contract.md +1 -1
  17. package/docs/system_specs.md +71 -21
  18. package/docs/theming.md +64 -0
  19. package/docs/training.md +26 -19
  20. package/docs/troubleshooting.md +76 -0
  21. package/docs/upgrading-hosts-to-3.md +42 -0
  22. package/examples/python/README.md +6 -0
  23. package/examples/python/base_policy_server.py +19 -0
  24. package/package.json +10 -3
  25. package/src/api/PaddockSimulatorController.js +150 -24
  26. package/src/api/runtimeTheme.js +59 -0
  27. package/src/app/F1SimulatorApp.js +213 -29
  28. package/src/app/banners/raceDataBannerController.js +71 -36
  29. package/src/app/banners/stewardMessageController.js +5 -4
  30. package/src/app/camera/cameraConstants.js +2 -0
  31. package/src/app/camera/cameraController.js +86 -29
  32. package/src/app/camera/cameraGeometry.js +12 -1
  33. package/src/app/domBindings.js +89 -9
  34. package/src/app/layoutScheduler.js +27 -0
  35. package/src/app/layoutSupport.js +165 -0
  36. package/src/app/raceOverlayClearanceSupport.js +158 -0
  37. package/src/app/readouts/carOverviewRenderer.js +16 -8
  38. package/src/app/readouts/controlStateRenderer.js +1 -1
  39. package/src/app/readouts/raceStatusRenderer.js +30 -21
  40. package/src/app/readouts/readoutFormatters.js +2 -0
  41. package/src/app/readouts/telemetryRenderer.js +17 -8
  42. package/src/app/readouts/timingTowerRenderer.js +262 -49
  43. package/src/app/runtime/frameLoop.js +5 -1
  44. package/src/app/runtime/runtimeVisibility.js +66 -5
  45. package/src/config/cssValues.js +16 -0
  46. package/src/config/defaultOptions.js +58 -38
  47. package/src/config/themeOptions.js +830 -0
  48. package/src/config/timingGapMode.js +13 -0
  49. package/src/config/urlOptions.js +25 -0
  50. package/src/data/championship.js +14 -9
  51. package/src/data/index.d.ts +212 -0
  52. package/src/data/index.js +14 -0
  53. package/src/environment/controllerLoop.js +17 -3
  54. package/src/environment/episode.js +11 -1
  55. package/src/environment/index.d.ts +1 -2
  56. package/src/environment/metrics.js +59 -16
  57. package/src/environment/observationVector.js +50 -33
  58. package/src/environment/observations.js +89 -20
  59. package/src/environment/options.js +1 -1
  60. package/src/environment/runtime.js +44 -11
  61. package/src/environment/scenarios.js +1 -1
  62. package/src/environment/sensors/carRays.js +48 -37
  63. package/src/environment/sensors/index.js +501 -97
  64. package/src/environment/sensors/indexedRayBands.js +134 -46
  65. package/src/environment/sensors/rayBandTrace.js +1171 -0
  66. package/src/environment/sensors/rayChannels.js +108 -0
  67. package/src/environment/sensors/rayConfig.js +71 -25
  68. package/src/environment/sensors/rayGeometry.js +23 -17
  69. package/src/environment/sensors/sensorTargets.js +67 -1
  70. package/src/environment/sensors/surfaceRays.js +167 -118
  71. package/src/environment/sensors/trackRays.js +157 -48
  72. package/src/index.d.ts +153 -9
  73. package/src/index.js +52 -3
  74. package/src/simulation/collisionGeometry.js +260 -96
  75. package/src/simulation/driver/driverController.js +1 -1
  76. package/src/simulation/driver/edgeRecovery.js +1 -1
  77. package/src/simulation/driver/racingControls.js +2 -2
  78. package/src/simulation/driver/rejoinControls.js +2 -2
  79. package/src/simulation/pit/pitFlow.js +21 -9
  80. package/src/simulation/pit/pitKinematics.js +2 -2
  81. package/src/simulation/pit/pitQueue.js +5 -3
  82. package/src/simulation/pit/pitRouteMovement.js +9 -4
  83. package/src/simulation/pit/pitRouting.js +79 -46
  84. package/src/simulation/pit/pitState.js +1 -0
  85. package/src/simulation/pit/pitTireService.js +7 -3
  86. package/src/simulation/race/raceFinish.js +25 -8
  87. package/src/simulation/race/raceOrder.js +189 -32
  88. package/src/simulation/race/raceProgress.js +129 -37
  89. package/src/simulation/race/raceSetup.js +4 -6
  90. package/src/simulation/race/raceStep.js +26 -14
  91. package/src/simulation/race/safetyCar.js +1 -1
  92. package/src/simulation/race/stalledDnf.js +1 -1
  93. package/src/simulation/raceSimulation.js +20 -17
  94. package/src/simulation/rules/collisionSteward.js +40 -23
  95. package/src/simulation/rules/rulesReview.js +77 -56
  96. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  97. package/src/simulation/timing/drsTiming.js +143 -30
  98. package/src/simulation/timing/gapEstimation.js +25 -3
  99. package/src/simulation/timing/lapTelemetry.js +138 -40
  100. package/src/simulation/timing/raceTiming.js +5 -1
  101. package/src/simulation/timing/sectorPerformance.js +104 -26
  102. package/src/simulation/timing/timingHistory.js +229 -14
  103. package/src/simulation/timing/timingLines.js +65 -7
  104. package/src/simulation/track/pitLaneLayout.js +30 -3
  105. package/src/simulation/track/pitLaneState.js +218 -19
  106. package/src/simulation/track/spatialQueries.js +66 -33
  107. package/src/simulation/track/trackMath.js +91 -24
  108. package/src/simulation/track/trackModel.js +2 -0
  109. package/src/simulation/track/trackQueryIndex.js +1468 -189
  110. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  111. package/src/simulation/track/trackQueryScratch.js +7 -0
  112. package/src/simulation/track/trackQueryStats.js +25 -0
  113. package/src/simulation/track/trackSnapshotJson.js +139 -0
  114. package/src/simulation/track/trackStatePolicy.js +249 -2
  115. package/src/simulation/vehicle/contactResolution.js +99 -62
  116. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  117. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  118. package/src/simulation/vehicle/runoffResponse.js +54 -29
  119. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  120. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  121. package/src/simulation/vehicle/vehiclePhysics.js +45 -30
  122. package/src/simulation/vehicle/vehicleSnapshots.js +35 -1
  123. package/src/simulation/vehicle/vehicleState.js +7 -1
  124. package/src/simulation/vehicle/wheelSurface.js +807 -92
  125. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  126. package/src/styles.css +1568 -519
  127. package/src/ui/bannerTemplates.js +38 -28
  128. package/src/ui/cameraControlsTemplate.js +16 -5
  129. package/src/ui/carOverviewTemplate.js +12 -5
  130. package/src/ui/raceCanvasTemplate.js +31 -8
  131. package/src/ui/raceControlsTemplate.js +27 -10
  132. package/src/ui/shellTemplate.js +3 -1
  133. package/src/ui/telemetryDrawerTemplate.js +23 -10
  134. package/src/ui/telemetryTemplates.js +58 -32
  135. package/src/ui/templateUtils.js +48 -0
  136. package/src/ui/timingTowerTemplate.js +29 -12
@@ -0,0 +1,50 @@
1
+ # Data Helpers
2
+
3
+ Use the data subpath for CSS-free host tooling, server-side data preparation, public TypeScript types, and procedural track helpers.
4
+
5
+ ```js
6
+ import {
7
+ DriverData,
8
+ createProceduralTrack,
9
+ formatDriverNumber,
10
+ normalizeSimulatorDrivers,
11
+ } from '@inventure71/paddockjs/data';
12
+ ```
13
+
14
+ The data subpath does not import DOM, PixiJS, package CSS, or browser app code. It is the correct path for raw Node checks and build-time scripts.
15
+
16
+ ## Driver And Entry Normalization
17
+
18
+ ```js
19
+ const drivers = [
20
+ { id: 'budget', name: 'Budget Buddy', color: '#ff2d55' },
21
+ ];
22
+
23
+ const entries = [
24
+ {
25
+ driverId: 'budget',
26
+ driverNumber: 71,
27
+ timingName: 'Budget',
28
+ driver: { pace: 52, racecraft: 74, aggression: 38, riskTolerance: 47, patience: 81, consistency: 86 },
29
+ vehicle: { id: 'bb-01', name: 'BB-01', power: 48, braking: 72, aero: 55, dragEfficiency: 66, mechanicalGrip: 63, weightControl: 58, tireCare: 82 },
30
+ },
31
+ ];
32
+
33
+ const normalized = normalizeSimulatorDrivers(drivers, { entries });
34
+ ```
35
+
36
+ Host websites pass the original `drivers` and `entries` to browser mounts. Tooling can use `normalizeSimulatorDrivers()` when it needs the resolved driver/car/team data before runtime.
37
+
38
+ ## Procedural Track Helper
39
+
40
+ ```js
41
+ const track = createProceduralTrack(4101, {
42
+ profile: 'training-short',
43
+ length: { minMeters: 900, maxMeters: 1800 },
44
+ pitLane: { enabled: false },
45
+ });
46
+ ```
47
+
48
+ Treat generated track definitions as read-only package data. If you need to experiment with mutable definitions, copy the data first.
49
+
50
+ See [Data Contract](data_contract.md) for full driver, entry, team, asset, rule, and callback shapes.
@@ -15,7 +15,6 @@ mountF1Simulator(root, {
15
15
  seed,
16
16
  trackSeed,
17
17
  trackGeneration,
18
- trackQueryIndex,
19
18
  warmup,
20
19
  totalLaps,
21
20
  physicsMode,
@@ -53,7 +52,6 @@ const simulator = createPaddockSimulator({
53
52
  seed,
54
53
  trackSeed,
55
54
  trackGeneration,
56
- trackQueryIndex,
57
55
  warmup,
58
56
  totalLaps,
59
57
  physicsMode,
@@ -138,7 +136,7 @@ result = env.step({
138
136
  `controlledDrivers` is required. It supports one or many externally controlled cars. Non-controlled participants use the built-in driver AI in the stable 1.0 environment API.
139
137
  `externalRenderer` is optional and observer-only. It can be a function or `{ onFrame(frame) }`, and receives `{ snapshot, observation, meta }` on `reset`, `step`, and `resetDrivers`. The hook does not mutate simulation state and hook failures are isolated so stepping continues.
140
138
  `warmup` is optional and enabled by default. It primes a disposable runtime during load/reset creation and caches by configuration fingerprint so repeated identical resets skip warmup. Use `warmup: { enabled, policy: 'config-change' | 'always' | 'never', steps }` or `warmup: false` to disable.
141
- `physicsMode` is optional and accepts `'arcade'` or `'simulator'`. Invalid values fall back to `'arcade'`, which is the default compatibility mode. `'simulator'` keeps the same public action contract but uses stricter 2D velocity/yaw dynamics and exposes additional telemetry fields on snapshots and observations: `velocityX`, `velocityY`, `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, and `stabilityState`. In simulator mode, `slipAngleRadians` is derived from the car heading versus actual velocity direction, and mixed wheel surfaces are averaged for physics while wheel snapshots still report each contact patch. Terminal and race-control-held simulator cars publish `velocityX: 0` and `velocityY: 0` whenever scalar `speed` is zero, so training loops do not see stale pre-DNF, pre-grid, pre-red-flag, or pre-queue motion. Arcade snapshots publish `velocityX` and `velocityY` as `null`; consumers that need a heading-aligned arcade velocity should derive it from `heading` and `speed`.
139
+ `physicsMode` is optional and accepts `'arcade'` or `'advanced'`. Invalid values, including the removed old `'simulator'` name, fall back to `'arcade'`, which is the default compatibility mode. `'advanced'` keeps the same public action contract but uses stricter 2D velocity/yaw dynamics and exposes additional telemetry fields on snapshots and observations: `velocityX`, `velocityY`, `lateralG`, `longitudinalG`, `gripUsage`, `slipAngleRadians`, `tractionLimited`, and `stabilityState`. In advanced mode, `slipAngleRadians` is derived from the car heading versus actual velocity direction, and mixed wheel surfaces are averaged for physics while wheel snapshots still report each contact patch. Terminal and race-control-held advanced-mode cars publish `velocityX: 0` and `velocityY: 0` whenever scalar `speed` is zero, so training loops do not see stale pre-DNF, pre-grid, pre-red-flag, or pre-queue motion. Arcade snapshots publish `velocityX` and `velocityY` as `null`; consumers that need a heading-aligned arcade velocity should derive it from `heading` and `speed`.
142
140
  `rules` is an optional override object for the race rules documented in [rules.md](rules.md). Flat keys such as `standingStart: false` still work for existing behavior. Advanced systems live under `rules.modules` so hosts can choose a preset and then override individual modules:
143
141
 
144
142
  ```js
@@ -303,7 +301,7 @@ Operationally this is one architecture with two modes:
303
301
  - local expert mode: browser owns stepping
304
302
  - external render mode: browser only renders authoritative external frames
305
303
 
306
- Policy Runner maps this directly through supported controller modes: `Distilled policy` and `Policy server` for browser-owned stepping, and `Live preview stream` for external render-only frames.
304
+ Policy Runner maps this directly through supported controller modes: `Distilled policy` and `Policy server` for browser-owned stepping, and `Live preview stream` for external render-only frames. The browser-owned HTTP policy-server transport uses protocol version `2`: `/policy/reset` receives static metadata such as `actionSpec`, `observationSpec`, configuration, `previousActionFields`, and `metricFields` once per session, while `/policy/decide-batch` receives only `driverIds` plus parallel compact `vectors`, `previousActions`, `metrics`, and `events` arrays aligned by driver index. Per-decision payloads intentionally do not include full observation objects, schemas, snapshots, or track/static metadata. Server integrations must treat the reset payload as the place to cache specs/configuration and tuple field order; Policy Runner server mode does not expose a legacy rich-observation transport option.
307
305
 
308
306
  `steering` is an absolute steering-wheel target: `-1` points at the maximum left steering limit, `0` points at center, `1` points at the maximum right steering limit, and intermediate values are percentages of that limit. The vehicle physics moves the steering angle toward that target through the configured steering-rate limit, so centering is physical rather than an instantaneous snap. `throttle` and `brake` are clamped from `0` to `1`. `steering`, `throttle`, and `brake` are required on every controlled-driver action; missing or non-finite values fail action validation instead of defaulting to zero.
309
307
 
@@ -314,7 +312,7 @@ Pit actions may also include `pitCompound` or `pitTargetCompound`, for example `
314
312
  The recommended runtime convention is a user-owned driver controller with `decideBatch(context) -> { [driverId]: { steering, throttle, brake, pitIntent? } }`. Controllers may load any model format or call any inference backend. PaddockJS only provides the simulator runtime context, stable controlled-driver ordering, cached specs, compact observations, and normalized action validation.
315
313
 
316
314
  ```js
317
- import { createPaddockDriverControllerLoop } from '@inventure71/paddockjs';
315
+ import { createPaddockDriverControllerLoop } from '@inventure71/paddockjs/environment';
318
316
 
319
317
  const controller = {
320
318
  async init(ctx) {
@@ -348,7 +346,7 @@ const actionSpec = env.getActionSpec();
348
346
  const observationSpec = env.getObservationSpec();
349
347
  ```
350
348
 
351
- `actionSpec` describes controlled drivers, normalized action ranges, and the optional pit intent values. `observationSpec` describes object observation fields, ray layout, nearby-car limits, track lookahead fields, and the versioned vector schema. `observation.lookaheadMeters` is sanitized to a finite numeric array; invalid or empty values fall back to the default `[20, 50, 100, 150]`. The opt-in `observation.profile: 'physical-driver'` profile defaults lookahead to `[]` so policies can use local driver-like senses without receiving privileged future track curvature. Realistic training runs should pair this profile with `physicsMode: 'simulator'`; that keeps the model's observed yaw, grip, contact-patch, kerb, and runoff behavior aligned with the physics it is learning to control.
349
+ `actionSpec` describes controlled drivers, normalized action ranges, and the optional pit intent values. `observationSpec` describes object observation fields, ray layout, nearby-car limits, track lookahead fields, and the versioned vector schema. `observation.lookaheadMeters` is sanitized to a finite numeric array; invalid or empty values fall back to the default `[20, 50, 100, 150]`. The opt-in `observation.profile: 'physical-driver'` profile defaults lookahead to `[]` so policies can use local driver-like senses without receiving privileged future track curvature. Realistic training runs should pair this profile with `physicsMode: 'advanced'`; that keeps the model's observed yaw, grip, contact-patch, kerb, and runoff behavior aligned with the physics it is learning to control.
352
350
 
353
351
  Observation output can be compacted for training throughput:
354
352
 
@@ -365,6 +363,17 @@ The default remains `output: 'full'`, `includeSchema: true`, and `vectorType: 'a
365
363
 
366
364
  The executable model-sense contract lives in `src/__tests__/environmentSenseContract.test.js` and is summarized in [Model Sense Contract](sense_contract.md). It treats the object observation as a policy-facing contract, checks it against simulator snapshot facts through independent unit and geometry formulas, then decodes each vector schema entry back to the object contract. A new vector field is not considered contract-covered until it has an oracle there.
367
365
 
366
+ Episode limits are configured separately from race length:
367
+
368
+ ```js
369
+ episode: {
370
+ maxSteps: 2400,
371
+ endOnRaceFinish: true,
372
+ }
373
+ ```
374
+
375
+ `episode.maxSteps` is a safety horizon for controlled-driver episodes, not the number of race laps. If all reported controlled drivers reach it, the result is `truncated: true`, `done: true`, and `endReason: 'max-steps'`. The default is intentionally high (`1_000_000` environment/expert steps) so normal browser expert sessions are unlikely to hit it; bounded training, evaluation, and smoke loops should pass a smaller explicit value. `episode.endOnRaceFinish` defaults to `true`, so race finish remains terminal unless a host explicitly disables that behavior.
376
+
368
377
  Result state output can also be compacted:
369
378
 
370
379
  ```js
@@ -571,7 +580,7 @@ snapshot.cars = physics-driven race participants
571
580
  snapshot.replayGhosts = trajectory-driven replay/reference entities
572
581
  ```
573
582
 
574
- Observation objects use physical units such as kph, meters/second, meters, and radians. Optional `vector` values use fixed documented scaling from `schema`; they do not use hidden per-car normalization. Full simulator truth remains available under `state.snapshot`. Internally, the environment avoids rebuilding full snapshots during each `frameSkip` substep and defaults to the non-public track query index unless `trackQueryIndex: false` is set, but the returned `state.snapshot`, reward callback `previous` snapshot, and reward callback `state.snapshot` keep the same public shape.
583
+ Observation objects use physical units such as kph, meters/second, meters, and radians. Optional `vector` values use fixed documented scaling from `schema`; they do not use hidden per-car normalization. Full simulator truth remains available under `state.snapshot`. Internally, the environment avoids rebuilding full snapshots during each `frameSkip` substep and always uses the non-public track query index for track, surface, pit-lane, and ray queries, but the returned `state.snapshot`, reward callback `previous` snapshot, and reward callback `state.snapshot` keep the same public shape.
575
584
 
576
585
  The environment observation now exposes local physical driver senses separately from full snapshot truth. `self.yawRateRadiansPerSecond` is the car's current yaw rate. `self.appliedControls` reports the normalized controls that actually drove the latest physics step: `steering` in `-1..1`, `steeringRadians` in simulator radians, `throttle` in `0..1`, and `brake` in `0..1`. For controlled cars this mirrors the accepted environment action; in `actionPolicy: 'report'` mode, a missing or invalid vehicle action releases stale manual controls, lets the built-in AI drive that step, and exposes its exact controls for auditing or clean local imitation datasets. `trackRelation` gives immediate local road relationship: lateral offset, heading error, legal width, left/right boundary distance, legal-surface state, and current surface. `contactPatches` exposes the four wheel/contact-patch surface readings as stable public observation data. Compact physical-driver vectors include each patch's `surfaceCode`, `signedOffsetMeters`, and `crossTrackErrorMeters` so vector-only training loops do not lose a contact-patch sense that exists in object observations.
577
586
 
@@ -627,9 +636,9 @@ sensors: {
627
636
 
628
637
  Ray precision defaults to `driver`. Driver precision is the active model-facing sensor contract and uses the normal sampled ray step without extra refinement. `precision: 'debug'` is available for clearly labeled diagnostics with additional edge refinement, but debug precision must not be displayed as model senses unless the policy is also running with that exact sensor config.
629
638
 
630
- Surface channels are computed only when requested. `kerb` is legal racing surface. `illegalSurface` reports the first grass or gravel surface crossed by the ray. Requested surface-ray channels are part of the active object observation and compact vector schema for both default and physical-driver profiles. For normal driver interactions, `precision: 'driver'` road-edge, kerb, and illegal-surface channels keep the sampled driver contract whether the track query index is enabled or disabled. The `batch-training` participant profile keeps its indexed recovery-ray path as its own high-throughput training contract. Other indexed ray-boundary shortcuts are reserved for non-driver/debug precision paths where sub-sample diagnostics are explicitly requested. Requested surface channels preserve an origin hit at `distanceMeters: 0` even if another requested channel has no visible boundary. Ambiguous pit-connector and unusual geometry cases may use sampled fallback internally, but the returned ray object shape is unchanged. Barrier walls are not exposed as a model-facing ray target; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Barrier contact is enforced as a shared destruction boundary in both physics modes at the rendered wall's inner face and reported through events, snapshots, metrics, and per-driver episode state.
639
+ Surface channels are computed only when requested. `kerb` is legal racing surface. `illegalSurface` reports the first grass or gravel surface crossed by the ray. Requested surface-ray channels are part of the active object observation and compact vector schema for both default and physical-driver profiles. `precision: 'driver'` road-edge, kerb, and illegal-surface channels use the canonical indexed track projection path with driver-precision validation, preserving zero-distance origin surface hits and avoiding debug-only refinement in policy inputs. Requested surface channels preserve an origin hit at `distanceMeters: 0` even if another requested channel has no visible boundary. Barrier-origin recovery rays stay on direct indexed band results whenever the tracer can already prove the misses and the first non-barrier illegal-surface entry; ambiguous pit-connector, pit-lane-origin, and remaining unusual geometry cases may still use sampled validation internally, but the returned ray object shape is unchanged. Barrier walls are not exposed as a model-facing ray target; active ray objects, vectors, schemas, and visualizations must not expose a `barrier` ray channel. Barrier contact is enforced as a shared destruction boundary in both physics modes at the rendered wall's inner face and reported through events, snapshots, metrics, and per-driver episode state.
631
640
 
632
- Nearby-car observations are car-relative. When simulator velocity fields are present, `closingRateMetersPerSecond` and `timeToContactSeconds` use the actual 2D velocity vector instead of assuming the car is moving in its heading direction, so slip and recovery states remain physically accurate:
641
+ Nearby-car observations are car-relative. When advanced-mode velocity fields are present, `closingRateMetersPerSecond` and `timeToContactSeconds` use the actual 2D velocity vector instead of assuming the car is moving in its heading direction, so slip and recovery states remain physically accurate:
633
642
 
634
643
  ```js
635
644
  {
@@ -731,9 +740,11 @@ Each `car.lapTelemetry` snapshot includes current/last/best lap and sector timin
731
740
 
732
741
  Profiles are presets, not separate generators. `race` preserves the default full-length circuit with pit lane; `training-short`, `training-medium`, and `training-technical` generate smaller pitless circuits for training or demos. Resolution is `race` defaults, selected profile defaults, then explicit overrides. Procedural tracks are generated by tracing, smoothing, and warping package-owned seeded region masks, then validating the resulting centerline instead of falling back to pure ovals. Invalid candidates are rejected for length, world bounds, non-adjacent clearance, turn sharpness, self-intersections, and weak shape variation. The start/finish area is normalized into an explicit straight window so the starting grid, pit-entry approach, and immediate pit-exit merge area are straight even when the rest of the circuit is generated from curved controls.
733
742
 
734
- `trackQueryIndex` controls indexed track/surface query acceleration. Browser/expert simulator mounts, headless environments, and direct race-simulation construction default this option to `true`, so visual race simulation, expert controls, Policy Runner playback, headless training/evaluation runs, and model-sense visualization use the indexed path by default. Set `trackQueryIndex: false` to force legacy non-indexed queries for comparison or debugging. It does not change public snapshot or observation shapes.
743
+ Indexed track/surface queries are the canonical runtime path for browser/expert simulator mounts, headless environments, direct race-simulation construction, Policy Runner playback, training/evaluation runs, and model-sense visualization. The index is internal infrastructure and does not change public snapshot or observation shapes.
744
+
745
+ `backLinkHref` is optional and controls the package-owned top-bar back link in the all-in-one shell and race-controls component. It accepts relative URLs, hash URLs, and absolute `http:` / `https:` URLs. Unsafe schemes such as `javascript:` and malformed URL values are replaced with the package default. Driver, team, snapshot, and radio colors are validated before they are written as runtime CSS values; unsafe color strings fall back to package defaults.
735
746
 
736
- `initialCameraMode` is optional and accepts `'overview'`, `'leader'`, `'selected'`, `'show-all'`, or `'pit'`. Invalid values fall back to `'leader'`. The `overview` camera frames the generated track bounds with package-owned padding and pit-lane extent. The `pit` camera frames the active track's `pitLane` geometry, zooms out when needed to keep the full pit lane inside the active race-view safe area, and falls back to `leader` when no pit lane is available. Camera zoom controls and wheel zoom apply to every mode, with zoom-out bounded by the active track frame.
747
+ `initialCameraMode` is optional and accepts `'overview'`, `'leader'`, `'selected'`, `'driver'`, `'show-all'`, or `'pit'`. Invalid values fall back to `'leader'`. The `overview` camera frames the 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 setting `initialCameraMode: 'driver'` enables that control automatically. The `pit` camera frames the active track's `pitLane` geometry, zooms out when needed to keep the full pit lane inside the active race-view safe area, and falls back to `leader` when no pit lane is available. Camera zoom controls and wheel zoom apply to every mode, with zoom-out bounded by the active track frame.
737
748
 
738
749
  Each driver must have:
739
750
 
@@ -815,6 +826,7 @@ Entries are optional. If omitted, defaults are used.
815
826
  name: 'Ledger Racing',
816
827
  color: '#00ff84',
817
828
  icon: 'LR',
829
+ theme: 'trackside',
818
830
  pitCrew: {
819
831
  speed: 0.72,
820
832
  consistency: 0.81,
@@ -870,6 +882,8 @@ Optional lifecycle callbacks:
870
882
 
871
883
  Race snapshots include a top-level `penalties` array. Each penalty entry includes `id`, `type`, `driverId`, `strictness`, `status`, `penaltySeconds`, `pendingPenaltySeconds`, `serviceType`, `serviceRequired`, `serviceServedAt`, `appliedAt`, `cancelledAt`, `unserved`, `positionDrop`, `gridDrop`, `disqualified`, `consequences`, `lap`, `at`, and rule-specific context such as `otherCarId`, `aheadDriverId`, `atFaultDriverId`, `sharedFault`, and `impactSpeedKph` for collision penalties or `speedKph`, `speedLimitKph`, `excessKph`, and `pitLanePart` for pit-lane speeding penalties. Clear rear contact has one at-fault driver; unclear meaningful contact records one shared-fault penalty per involved driver. Multiple time penalties for the same driver are summed into the car snapshot's `penaltySeconds` and adjusted finish/classification time.
872
884
 
885
+ Each full public car snapshot includes `trackState` for the car-center track classification used by runtime readouts and environment surfaces. Its stable public fields are `distance`, `signedOffset`, `crossTrackError`, `surface`, `inPitLane`, `pitLanePart`, `pitBoxId`, `curvature`, and `heading`. `pitLanePart` and `pitBoxId` are `null` when the car is not classified on a pit-lane part or pit box. The existing flat `signedOffset`, `crossTrackError`, `surface`, `inPitLane`, `pitLanePart`, and `pitBoxId` fields remain as convenience mirrors for common UI and host checks.
886
+
873
887
  Penalty status values are `issued`, `served`, `applied`, and `cancelled`. Time, position-drop, grid-drop, and disqualification consequences are immediate `applied` penalties. Drive-through and stop-go consequences are service penalties: they start as `issued`, can be completed with `servePenalty(penaltyId)`, and convert to applied time if unserved when final classification is calculated. Pit stops also serve eligible penalties before tire work starts: applied time penalties add their seconds as a hold, stop-go penalties add their configured service seconds, and drive-through penalties are marked served by the pit-lane traversal without extra stationary hold time.
874
888
 
875
889
  ## Asset Overrides
@@ -935,6 +949,9 @@ ui: {
935
949
  raceDataBannerSize: 'custom',
936
950
  raceDataTelemetryDetail: false,
937
951
  timingTowerVerticalFit: 'expand-race-view',
952
+ timingGapMode: 'interval',
953
+ timingGapModeToggle: true,
954
+ timingEntryVerticalPadding: 5,
938
955
  },
939
956
  debug: {
940
957
  physicsModeIndicator: false,
@@ -942,11 +959,11 @@ debug: {
942
959
  ```
943
960
 
944
961
  - `layoutPreset`: `'standard'` or `'left-tower-overlay'`. The overlay preset creates a left broadcast gutter inside the race canvas, places the timing tower in that gutter at the same width as the default timing-board column, and frames the camera around the remaining race-view area. Camera controls are external by default. In the combined shell, the project/radio lower-third stays inside the race window and can render over the timing sidebar.
945
- - `cameraControls`: `'embedded'`, `'external'`, or `false`. The default is external so camera controls do not cover the race view. Embedded controls render inside the race canvas only when explicitly requested. External controls are mounted with `mountCameraControls(root)` or included in package-owned workbench templates. The generated controls include mode buttons, zoom buttons, and a `Mute banners` toggle that temporarily disables project/radio lower-thirds while active. A browser-playback speed button is optional for normal camera controls through `ui.simulationSpeedControl: true`; the complete race workbench enables it by default and cycles `1x`, `2x`, `3x`, `4x`, `5x`, `10x`, then back to `1x`. `false` leaves camera controls unrendered, though callers can still drive selection through controller methods.
962
+ - `cameraControls`: `'embedded'`, `'external'`, or `false`. The default is external so camera controls do not cover the race view. Embedded controls render inside the race canvas only when explicitly requested. External controls are mounted with `mountCameraControls(root)` or included in package-owned workbench templates. The generated controls include mode buttons, zoom buttons, and a `Mute banners` toggle that temporarily disables project/radio lower-thirds while active. `ui.driverCamera: true` adds the opt-in `driver` camera button; it is omitted from generated controls by default. A browser-playback speed button is optional for normal camera controls through `ui.simulationSpeedControl: true`; the complete race workbench enables it by default and cycles `1x`, `2x`, `3x`, `4x`, `5x`, `10x`, then back to `1x`. `false` leaves camera controls unrendered, though callers can still drive selection through controller methods.
946
963
  - `showFps`: controls whether the race canvas renders the FPS readout.
947
964
  - `showRaceDataPanel`: controls whether the precombined shell includes the project/radio lower-third inside the race window.
948
965
  - `showTimingTower`, `showTelemetry`: reserved component visibility flags for host layout decisions.
949
- - `debug.physicsModeIndicator`: when `true`, renders a small top-left square in the race canvas. Blue means `physicsMode: 'arcade'`; red means `physicsMode: 'simulator'`. It defaults to `false` and is intended only for debug/development use.
966
+ - `debug.physicsModeIndicator`: when `true`, renders a small top-left square in the race canvas. Blue means `physicsMode: 'arcade'`; red means `physicsMode: 'advanced'`. It defaults to `false` and is intended only for debug/development use.
950
967
  - `telemetryIncludesOverview`: controls whether the telemetry stack template embeds the car/driver overview. Composable hosts can also pass `mountTelemetryPanel(root, { includeOverview: false })`.
951
968
  - `telemetryModules`: controls optional telemetry surfaces inside stack/drawer templates. The default object enables `core` scalar readouts, `sectors` progress bars, `lapTimes`, and `sectorTimes`. It can also be `false` to disable all telemetry modules, or an array such as `['sectors', 'lapTimes']` to render only named modules. These modules are also individually mountable with `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetryLapTimes`, and `mountTelemetrySectorTimes`.
952
969
  - `raceDataBanners.initial`: `'project'`, `'radio'`, or `'hidden'`. This controls which lower-third appears first in the precombined shell.
@@ -957,29 +974,84 @@ debug: {
957
974
  - `penaltyBanners`: when `true`, the race view shows a top steward message for track-limit warnings and new steward penalty decisions. Time penalties show a large `+10s` style chip in the left block, with the affected car and rule/reason beside it. Warning-only messages use warning colors and remain separate from penalty decisions. It does not replace the project/radio lower-third.
958
975
  - `timingPenaltyBadges`: when `true`, timing rows for penalized drivers show a red `!` badge with an accessible penalty label. Warning-only events do not count as penalties and do not show the badge.
959
976
  - `timingTowerVerticalFit`: `'expand-race-view'` lets the combined race window grow to contain the timing tower. `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same values can be passed to `mountRaceCanvas(root, { includeTimingTower: true, timingTowerVerticalFit })` for an embedded composable timing tower.
977
+ - `timingGapMode`: `'interval'` or `'leader'`. The default `'interval'` starts the tower in `Int` mode and shows the interval to the car ahead; `'leader'` starts the tower in `Gap` mode and shows the total gap to P1. Invalid values fall back to `'interval'`.
978
+ - `timingGapModeToggle`: defaults to `true`. When `true`, the timing tower renders one compact `Int`/`Gap` header toggle in the timing header's gap column. The toggle is package-owned UI: it shares the header alignment grid with `POS`, `TEAM`, `PROJECT`, and `TYRE`, preserves the timing tower's narrow broadcast proportions, keeps a practical `44px` target, and draws selected/focus styling inside the header instead of expanding the column. Set it to `false` when the host should control the mode only through `setTimingGapMode()`, `getTimingGapMode()`, or `toggleTimingGapMode()`.
979
+ - `timingEntryVerticalPadding`: defaults to `5`. This single non-negative number is applied in pixels above and below every timing row entry, letting hosts make the timing tower denser or looser without per-entry styling.
980
+ - `responsiveNarrowLayout`: defaults to `true`. Package templates use it to enable the narrow timing reveal and narrow drawer behavior when their container is too small for the wide layout. In responsive narrow race canvases, PaddockJS measures timing-row entries and the lower-third before adding bottom clearance, so extra race height is reserved only when the banner would cover timing entries. Set it to `false`, or pass `responsiveNarrowLayout: false` to `mountRaceCanvas()` / `mountRaceTelemetryDrawer()`, only when a host intentionally wants the non-responsive stacked behavior.
960
981
 
961
982
  No UI option exists for raw timing-tower width, max width, or horizontal ratio. The timing tower is capped by the package CSS variable `--timing-board-max-width` because very wide timing boards read poorly. Host pages can scale the whole simulator by changing the mount container, but package-owned layout presets keep their internal proportions inside PaddockJS. For standalone timing towers, give the mount root a fixed height when a fixed vertical footprint is needed; the package keeps the frame inside that height and scrolls only the timing entries. Narrow hosts are handled internally: side-gutter timing towers become stacked/full-width, embedded timing towers stop behaving like desktop side overlays, and the camera safe area stops reserving a left gutter when the measured timing board is effectively full-width.
962
983
 
984
+ Timing gap display is runtime state on the simulator/controller, not a property of the rendered button. `ui.timingGapMode` only chooses the initial value. After mounting, `setTimingGapMode('interval' | 'leader')` updates all mounted timing towers immediately, `getTimingGapMode()` reports the current value, and `toggleTimingGapMode()` switches to the other supported value. Hiding the manual toggle does not disable those methods; it only removes the package-owned header affordance so a host can provide its own external control.
985
+
963
986
  ## Theme And Sizing Contract
964
987
 
965
988
  ```js
966
989
  theme: {
967
- accentColor: '#e10600',
968
- greenColor: '#14c784',
969
- yellowColor: '#ffd166',
990
+ mode: 'system',
991
+ use: 'trackside',
992
+ tokens: {
993
+ primary: { light: '#c90400', dark: '#e10600' },
994
+ primaryText: '#ffffff',
995
+ pitLane: '#7c3aed',
996
+ },
997
+ themes: {
998
+ trackside: {
999
+ extends: 'default',
1000
+ tokens: {
1001
+ yellowFlag: { dark: '#ffcc00' },
1002
+ },
1003
+ components: {
1004
+ button: {
1005
+ background: 'pitLane',
1006
+ text: 'primaryText',
1007
+ border: 'primary',
1008
+ },
1009
+ },
1010
+ },
1011
+ },
1012
+ componentThemes: {
1013
+ 'race-controls': 'trackside',
1014
+ 'timing-tower': 'selectedTeam',
1015
+ },
1016
+ teamThemes: {
1017
+ ferrari: 'trackside',
1018
+ },
970
1019
  timingTowerMaxWidth: '390px',
971
1020
  raceViewMinHeight: '620px',
972
1021
  }
973
1022
  ```
974
1023
 
975
- These values are applied as package CSS variables:
976
-
977
- - `accentColor` -> `--paddock-accent-color`
978
- - `greenColor` -> `--paddock-green-color`
979
- - `yellowColor` -> `--paddock-yellow-color`
1024
+ Themes may be partial, but resolved themes are always complete. The package `default` theme owns the token schema; named themes in `theme.themes` are derivative packages that can override only those known tokens. Unknown theme tokens and unknown component slots are ignored instead of becoming new CSS variables. If a token is supplied only for `light` or only for `dark`, PaddockJS generates and caches the opposite-mode value in the resolved theme. `theme.use` selects the active named package. `theme.componentThemes` maps `data-paddock-component` names such as `race-controls`, `camera-controls`, `timing-tower`, `race-canvas`, and `race-data-panel` to `default`, `active`, `selectedTeam`, `team`, `team:<id>`, or a named theme package. Component theme keys may also use camelCase aliases such as `raceControls` and `timingTower`; `selectedDriverPanel` targets both selected-driver package surfaces. `theme.teamThemes` maps team ids to selectors for team-aware surfaces; `entries[*].team.theme` feeds the same map, and explicit `theme.teamThemes` values override team metadata when both are supplied. Selected-driver surfaces such as the car/driver overview and race-data panel use the selected team's theme by default when one is available; structural controls keep the active global theme unless explicitly overridden. Theme and driver/team colors are validated before they are written to CSS variables.
1025
+
1026
+ Supported theme tokens are applied as package CSS variables:
1027
+
1028
+ - `primary` -> `--paddock-color-primary`
1029
+ - `primaryText` -> `--paddock-color-primary-text`
1030
+ - `secondary` -> `--paddock-color-secondary`
1031
+ - `secondaryText` -> `--paddock-color-secondary-text`
1032
+ - `surface` -> `--paddock-color-surface`
1033
+ - `surfaceRaised` -> `--paddock-color-surface-raised`
1034
+ - `surfacePanel` -> `--paddock-color-surface-panel`
1035
+ - `text` -> `--paddock-color-text`
1036
+ - `mutedText` -> `--paddock-color-muted-text`
1037
+ - `border` -> `--paddock-color-border`
1038
+ - `success` -> `--paddock-color-success`
1039
+ - `warning` -> `--paddock-color-warning`
1040
+ - `danger` -> `--paddock-color-danger`
1041
+ - `info` -> `--paddock-color-info`
1042
+ - `yellowFlag` -> `--paddock-color-yellow-flag`
1043
+ - `greenFlag` -> `--paddock-color-green-flag`
1044
+ - `redFlag` -> `--paddock-color-red-flag`
1045
+ - `safetyCar` -> `--paddock-color-safety-car`
1046
+ - `drsActive` -> `--paddock-color-drs-active`
1047
+ - `pitLane` -> `--paddock-color-pit-lane`
1048
+ - `track` -> `--paddock-color-track`
1049
+ - `trackEdge` -> `--paddock-color-track-edge`
980
1050
  - `timingTowerMaxWidth` -> `--paddock-timing-tower-max-width`
981
1051
  - `raceViewMinHeight` -> `--paddock-race-view-min-height`
982
1052
 
1053
+ The old `accentColor`, `greenColor`, `yellowColor`, and related `*Color` fields remain compatibility aliases that feed the semantic tokens and legacy CSS variables such as `--paddock-accent-color`.
1054
+
983
1055
  Prefer these fields over host CSS overrides. They are the stable styling surface for reusable embeds.
984
1056
 
985
1057
  ## Race Completion Snapshot
@@ -1036,6 +1108,8 @@ Every built track is automatically divided into three equal sectors. `snapshot.t
1036
1108
 
1037
1109
  Every built track also exposes hidden `snapshot.track.timingLines`. Timing lines are spaced from the track length at an F1-style mini-sector target of roughly `150m..200m`; they are simulation metadata for gap calculation and are not rendered by default.
1038
1110
 
1111
+ In-memory snapshots keep the live track object so package code and host-side geometry helpers can still read full `samples`, pit-lane bounds, and internal runtime metadata. JSON serialization is intentionally leaner: `JSON.stringify(snapshot.track)` reuses a cached public geometry view, rounds static numeric track data to bounded precision, decimates the serialized `samples` polyline to a compact export path, encodes those exported samples as `[x, y, distance, heading, normalX, normalY, curvature]` rows under `track.sampleSchema`, and omits engine-only pit query helpers such as `pitLane.bounds`, `pitLane.boxBounds`, `pitLane.connectorBounds`, and pit-crew metadata. JSON pit-box and service-area exports also drop duplicated team metadata because the canonical team table already lives under `pitLane.teams`. This affects JSON/export/transport size only; it does not change the live JavaScript snapshot shape.
1112
+
1039
1113
  Tracks whose resolved generation options enable pit lanes expose `snapshot.track.pitLane`. The `race` profile includes this geometry; training profiles are pitless by default unless explicitly overridden. When present, the pit lane is deterministic for the track seed and contains:
1040
1114
 
1041
1115
  - `entry`: track distance before the start line, the true track `edgePoint`, an overlapping lane-facing `trackConnectPoint` on the track surface, connector points from the racing surface to the pit lane, and a procedural `roadCenterline` that is tangent to the main track at entry and tangent to the straight pit lane at the pit-lane start.
@@ -1044,8 +1118,8 @@ Tracks whose resolved generation options enable pit lanes expose `snapshot.track
1044
1118
  - `workingLane`: parallel pit box lane start/end points, offset, width, and centerline points.
1045
1119
  - `exit`: connector points from the pit lane back to the racing surface after the start line, plus the true track `edgePoint`, an overlapping lane-facing `trackConnectPoint` on the track surface, and a procedural `roadCenterline` that is tangent to the straight pit lane at pit-lane end and tangent to the main track at merge.
1046
1120
  - `teams`: team pit groups with id, name, color, index, the two assigned garage box ids, and one shared service area id.
1047
- - `serviceAreas`: 10 team service areas, one per team, each with center, queue point, corners, queue corners, team index, and optional `teamId`, `teamName`, and `teamColor`.
1048
- - `boxes`: 20 unused garage boxes as 10 team pairs, each with center, lane target, corners, team index, box index, and optional `teamId`, `teamName`, and `teamColor`.
1121
+ - `serviceAreas`: 10 team service areas, one per team, each with center, queue point, corners, queue corners, and team index. The live object also keeps optional `teamId`, `teamName`, and `teamColor`; JSON export drops those duplicates because `pitLane.teams` is the canonical source.
1122
+ - `boxes`: 20 unused garage boxes as 10 team pairs, each with center, lane target, corners, team index, and box index. The live object also keeps optional `teamId`, `teamName`, and `teamColor`; JSON export drops those duplicates because `pitLane.teams` is the canonical source.
1049
1123
 
1050
1124
  Pit-lane road and box states are legal drivable surfaces. Track state may report `surface: 'pit-entry'`, `'pit-lane'`, `'pit-exit'`, or `'pit-box'` with `inPitLane: true`; `crossTrackError` remains the distance from the main race track for compatibility, while pit-specific offsets are exposed as pit-lane fields on the internal state.
1051
1125
 
@@ -1104,26 +1178,29 @@ Controller methods:
1104
1178
  - `getPitIntent(driverId)`: reads the current pit intent.
1105
1179
  - `getPitTargetCompound(driverId)`: reads the current pit target tire.
1106
1180
  - `getSimulationSpeed()`: returns the active browser playback multiplier from the package-owned simulation-speed control, defaulting to `1`.
1181
+ - `setTimingGapMode(mode)`: sets the timing tower gap mode to `'interval'` or `'leader'` and immediately re-renders mounted timing towers. Invalid values fall back to `'interval'`.
1182
+ - `getTimingGapMode()`: returns the current timing tower gap mode.
1183
+ - `toggleTimingGapMode()`: switches between `'interval'` and `'leader'` and returns the active mode.
1107
1184
  - `servePenalty(penaltyId)`: marks an issued drive-through or stop-go penalty as served.
1108
1185
  - `cancelPenalty(penaltyId)`: cancels a penalty so it no longer affects service, timing, grid, or classification.
1109
1186
  - `getSnapshot()`: returns the latest simulation snapshot.
1110
1187
 
1111
- Composable controllers additionally expose:
1188
+ Composable controllers additionally expose the canonical composable mounting API:
1112
1189
 
1113
1190
  - `mountRaceControls(root)`: renders the top control/header component.
1114
1191
  - `mountCameraControls(root)`: renders package-owned camera mode, zoom, and project/radio banner mute controls outside the race canvas.
1115
1192
  - `mountSafetyCarControl(root)`: renders a package-owned safety-car button that binds to the same race-control state as other safety buttons.
1116
1193
  - `mountTimingTower(root)`: renders the timing tower component. The tower includes one hidden race-control status banner slot above the timing rows; `raceControl.mode: 'safety-car'` shows the yellow safety-car status, and `raceControl.mode: 'red-flag'` shows the red red-flag status.
1117
- - `mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit })`: renders the PixiJS canvas host, optional FPS, start lights, and the top steward message. Camera controls are external by default and render inside the race canvas only when `ui.cameraControls: 'embedded'` is explicitly requested. Pass `includeRaceDataPanel: true` to place the project/radio lower-third inside the race window so it shares race-canvas clipping and layering. Pass `includeTelemetrySectorBanner: true` only when the host intentionally wants the independent sector lower-third in addition to the project/radio banner. Pass `includeTimingTower: true` to place the timing tower inside the race canvas; `timingTowerVerticalFit: 'expand-race-view'` grows the canvas to the tower height, while `'scroll'` keeps the canvas height and scrolls timing rows inside the tower frame. This is required before `start()`.
1194
+ - `mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit, responsiveNarrowLayout })`: renders the PixiJS canvas host, optional FPS, start lights, and the top steward message. Camera controls are external by default and render inside the race canvas only when `ui.cameraControls: 'embedded'` is explicitly requested. Pass `includeRaceDataPanel: true` to place the project/radio lower-third inside the race window so it shares race-canvas clipping and layering. Pass `includeTelemetrySectorBanner: true` only when the host intentionally wants the independent sector lower-third in addition to the project/radio banner. Pass `includeTimingTower: true` to place the timing tower inside the race canvas; `timingTowerVerticalFit: 'expand-race-view'` grows the canvas to the tower height, while `'scroll'` keeps the canvas height and scrolls timing rows inside the tower frame. Responsive narrow timing reveal behavior is enabled by default and can be disabled with `responsiveNarrowLayout: false`. This is required before `start()`.
1118
1195
  - `mountTelemetryPanel(root, { includeOverview })`: renders the package-owned telemetry stack template. The stack is only a composition of detached telemetry surfaces, owns vertical scrolling when its host is shorter than its contents, and includes the car/driver overview by default unless `includeOverview: false` is passed or `ui.telemetryIncludesOverview` is `false`.
1119
1196
  - `mountTelemetryCore(root)`: renders selected-car scalar telemetry only.
1120
1197
  - `mountTelemetrySectors(root)`: renders the live sector progress graph only.
1121
1198
  - `mountTelemetryLapTimes(root)`: renders current, last, and best lap timing only.
1122
1199
  - `mountTelemetrySectorTimes(root)`: renders last and best sector timing only.
1123
- - `mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen, raceDataTelemetryDetail })`: renders a template that combines an external top control row, race canvas, embedded timing tower, the project/radio lower-third, top steward message, safety-car control, and a right-side telemetry drawer. The control row contains camera controls, the `1x..10x` simulation-speed toggle, banner mute, the safety-car button, and the telemetry toggle so those controls do not cover the race view. Pass `raceDataTelemetryDetail: true` when this template should put compact S1/S2/S3 detail in the project lower-third instead of mounting a second sector popup. The drawer embeds the same package-owned telemetry stack used by `mountTelemetryPanel()` and takes layout space from the race window when opened.
1200
+ - `mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen, raceDataTelemetryDetail, responsiveNarrowLayout })`: renders a template that combines an external top control row, race canvas, embedded timing tower, the project/radio lower-third, top steward message, safety-car control, and a right-side telemetry drawer. The control row contains camera controls, the `1x..10x` simulation-speed toggle, banner mute, the safety-car button, and the telemetry toggle so those controls do not cover the race view. Pass `raceDataTelemetryDetail: true` when this template should put compact S1/S2/S3 detail in the project lower-third instead of mounting a second sector popup. The drawer embeds the same package-owned telemetry stack used by `mountTelemetryPanel()` and takes layout space from the race window when opened on wide containers; narrow drawer reveal behavior is enabled by default, stacks the top workbench controls into readable full-width rows, and can be disabled with `responsiveNarrowLayout: false`.
1124
1201
  - `mountCarDriverOverview(root)`: renders the package-owned car/driver overview as a separate component with a Car/Driver toggle, center visual, and linked stat cells from the existing driver/vehicle rating components.
1125
1202
  - `mountRaceDataPanel(root)`: renders the project/race-data lower-third as a separate component for hosts that intentionally want it outside the race canvas.
1126
1203
  - `querySelector(selector)` / `querySelectorAll(selector)`: search across the mounted package-owned composable roots. These exist for integration tests and advanced host glue; ordinary hosts should prefer explicit controller methods and mounted component roots.
1127
1204
  - `start()`: initializes PixiJS, binds mounted controls, and starts the simulation loop.
1128
1205
 
1129
- Mount component roots before calling `start()`. If a component is not mounted, the runtime skips that UI surface instead of requiring hidden placeholder DOM. Mounted surfaces render a package-owned loading overlay immediately; `start()` removes those overlays after PixiJS, assets, controls, and initial readouts have initialized.
1206
+ Mount component roots before calling `start()`. If a component is not mounted, the runtime skips that UI surface instead of requiring hidden placeholder DOM. Mounted surfaces render a package-owned loading overlay immediately; `start()` removes those overlays after PixiJS, assets, controls, and initial readouts have initialized. If `restart(nextOptions)` is called before `start()`, mounted component markup is refreshed from the new resolved options so copy, component visibility, driver data, and theme changes do not leave stale pre-start HTML behind.
@@ -0,0 +1,75 @@
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
+ ## CSS
42
+
43
+ Most browser bundlers consume the root package stylesheet automatically. If your host build does not, import the stylesheet explicitly from a browser entry:
44
+
45
+ ```js
46
+ import '@inventure71/paddockjs/styles.css';
47
+ ```
48
+
49
+ 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.
50
+
51
+ 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.
52
+
53
+ ## Assets
54
+
55
+ 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.
56
+
57
+ ## Container Sizing
58
+
59
+ PaddockJS supports mount roots at least `320px` wide. Use normal responsive host CSS:
60
+
61
+ ```css
62
+ .simulator-section {
63
+ width: 100%;
64
+ min-width: 0;
65
+ }
66
+ ```
67
+
68
+ If a package surface is constrained below its supported size, PaddockJS shows an `Unsupported size` placeholder rather than clipping controls or overlapping race UI.
69
+
70
+ ## Where To Go Next
71
+
72
+ - Use [Composable Layouts](composable-layouts.md) when the host needs separate package-owned surfaces.
73
+ - Use [Theming](theming.md) for runtime light/dark sync and token customization.
74
+ - Use [Data Contract](data_contract.md) for full driver, entry, rule, UI, callback, and asset option shapes.
75
+ - 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.
@@ -12,15 +24,17 @@ Use these docs as the package source of truth:
12
24
  - [Model Sense Contract](sense_contract.md): executable proof boundary for policy-facing observation senses.
13
25
  - [Custom Model Controller Guide](custom_model_controller.md): how to wrap a trained model as a batched driver controller for browser playback or headless loops.
14
26
  - [Architecture](architecture.md): module ownership and data/control flow.
27
+ - [Component Inventory](component_inventory.md): package-owned UI surfaces, shared bases, derived/mixed variants, and remaining intentional duplication.
15
28
  - [Data Model Notes](data_model.md): lower-level simulator data model notes kept with package docs.
16
29
  - [Learnings](learnings.md): implementation lessons and layout verification rules learned from simulator integration issues.
17
30
 
18
31
  Common paths:
19
32
 
20
- - 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).
21
- - Building a package-owned composable layout: read the public API and returned-controller 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).
22
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.
23
- - 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.
24
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.
25
39
 
26
40
  When simulator behavior changes, update the relevant doc in this folder in the same change.
package/docs/learnings.md CHANGED
@@ -48,7 +48,7 @@ Unit tests should cover the contract, but browser geometry checks catch CSS inte
48
48
 
49
49
  ## Indexed Track Queries Must Reach Every Runtime Path
50
50
 
51
- The track query index is internal performance infrastructure, not a separate sensor contract. When a runtime opts into it, every simulation instance used by that runtime must receive the same option:
51
+ The track query index is internal performance infrastructure, not a separate sensor contract. Every simulation instance used by a runtime must own an index:
52
52
 
53
53
  - Headless environments created by `createPaddockEnvironment()`.
54
54
  - Browser expert mode through `F1SimulatorApp.createRaceSimulation()`.
@@ -56,14 +56,14 @@ The track query index is internal performance infrastructure, not a separate sen
56
56
  - Local preview remount/restart paths.
57
57
  - Any benchmark or smoke path used to validate training behavior.
58
58
 
59
- Do not assume that setting `trackQueryIndex: true` on a high-level preview or environment config is enough. Trace the option all the way into `createRaceSimulation()` and verify that the live simulation owns a non-enumerable `track.queryIndex`. The index must stay out of public JSON contracts: it may exist on the in-memory track object, but `Object.keys(snapshot.track)` must not expose `queryIndex`.
59
+ Do not assume that a high-level preview or environment config controls the query implementation. The runtime must always build a non-enumerable `track.queryIndex` for live simulations. The index must stay out of public JSON contracts: it may exist on the in-memory track object, but `Object.keys(snapshot.track)` must not expose `queryIndex`.
60
60
 
61
- The failure mode to avoid: headless training uses indexed queries and looks fast, while the browser visual/expert simulation silently drops the option and falls back to legacy `nearestSampleInRange()` scans near barriers, recovery starts, or far-off-track positions. This can make the Policy Runner lag even when the model, heuristic controller, and ray contract are correct.
61
+ The failure mode to avoid: headless training uses indexed queries and looks fast, while the browser visual/expert simulation bypasses the canonical index near barriers, recovery starts, or far-off-track positions. This can make the Policy Runner lag even when the model, heuristic controller, and ray contract are correct.
62
62
 
63
63
  Verification for index-related changes should include both layers:
64
64
 
65
- - A focused unit test proving the app or environment path forwards `trackQueryIndex` into the created simulation.
66
- - A browser reproduction or smoke check against the Policy Runner or relevant visual runtime, especially near barrier/recovery geometry where legacy nearest-sample scans are most expensive.
67
- - A profile or timing check when the bug is performance-related. If `nearestSampleInRange()` is still hot in an indexed browser path, the option did not reach the live simulation or another code path is bypassing the index.
65
+ - A focused unit test proving the app or environment path owns a live `track.queryIndex`.
66
+ - A browser reproduction or smoke check against the Policy Runner or relevant visual runtime, especially near barrier/recovery geometry where broad nearest-sample scans are most expensive.
67
+ - A profile or timing check when the bug is performance-related. If broad nearest-sample scans are still hot in a browser path, another code path is bypassing the index.
68
68
 
69
69
  Do not fix browser performance by changing model-facing sensor precision, hiding observations, or showing debug-only values. The active observation contract remains the source of truth: policies receive it, Policy Runner visualizes it, and the index only changes how the same values are computed.