@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
@@ -108,10 +108,12 @@ Responsibilities:
108
108
  - `options.js` validates `controlledDrivers`, scenario participants, reset placement config, frame skip, action policy, sensor config, and episode limits.
109
109
  - `actions.js` maps normalized public controls onto simulator steering/throttle/brake controls plus pit intent and optional target compound requests. It treats steering, throttle, and brake as required vehicle-control fields so broken model output is reported instead of silently becoming zeroed controls.
110
110
  - `scenarios.js` owns environment reset placement presets, absolute placements, and relative traffic layouts. It applies them through simulator state APIs during environment creation/reset only; it must not create policy-time teleport or assisted-control paths.
111
- - `observations.js`, `observationVector.js`, `sensors.js`, and `events.js` build sensor-style observations, versioned fixed-schema vectors, track lookahead/curvature, ray/nearby-car readings, and global/per-driver events. `observations.js` owns object observation assembly and direct compact-source preparation, while `observationVector.js` is the single owner of model-facing vector order, schema entries, and typed-array formatting for both object-backed and direct vector paths. Observation output formatting is separate from object/vector construction so compact training loops can request vector-only results while `getObservationSpec()` remains the schema source. Vector-only mode uses a direct vector path and can return `Float32Array` buffers without building the full public object observation. When a loop requests vector-only, schema-free, no-state output, `runtime.js` uses the internal `snapshotTraining()` path instead of the heavier public observation snapshot; returned state still uses public snapshot shapes. Environment-owned simulations default to the internal track query index unless `trackQueryIndex: false` is set, because repeated track/surface/ray fallback queries otherwise dominate step cost. `sensors.js` is a compatibility barrel; nearby-car scans, ray defaults, ray config normalization, track rays, surface rays, indexed ray-band intersections, car rays, body senses, contact-patch senses, boundary senses, opponent radar, and ray geometry live under `src/environment/sensors/`. Expensive surface-ray channels are opt-in and appear in the vector schema whenever requested. Replay-ghost sensor type flags are also part of compact ray/nearby-car vectors so vector-only consumers can distinguish real cars from opt-in references. Driver rays use a precision-aware cached origin state and shared per-observation ray target context. Normal driver `precision: 'driver'` road-edge, kerb, and illegal-surface channels keep the sampled values that policies receive with or without the track query index. The `batch-training` participant profile keeps the indexed recovery-ray path as its high-throughput training contract, and other indexed ray-boundary shortcuts are reserved for non-driver/debug precision paths. Destroyed/out-of-race cars keep the active ray schema but short-circuit to miss-valued rays before creating track ray contexts, which prevents terminal cars in batch training from repeatedly querying far-out geometry. Pit-connector and unusual cases still fall back to sampled checks. Ray precision defaults to the sampled model-facing `driver` contract; `debug` precision is diagnostic-only and must not be silently substituted into policy inputs or model-sense visualization.
112
- - `metrics.js` owns reward-neutral per-driver facts such as progress delta, legal progress delta, off-track/severe-cut state, destroyed state, low-speed state, spin/backwards/instability state, lap completion, lap time, and contact count. Contact metrics dedupe the identifiers on each normalized physical contact event so one event increments each involved driver once. It must not encode or choose rewards.
111
+ - `observations.js`, `observationVector.js`, `sensors.js`, and `events.js` build sensor-style observations, versioned fixed-schema vectors, track lookahead/curvature, ray/nearby-car readings, and global/per-driver events. `observations.js` owns object observation assembly and direct compact-source preparation, while `observationVector.js` is the single owner of model-facing vector order, schema entries, and typed-array formatting for both object-backed and direct vector paths. Observation output formatting is separate from object/vector construction so compact training loops can request vector-only results while `getObservationSpec()` remains the schema source. Vector-only mode uses a direct vector path and can return `Float32Array` buffers without building the full public object observation; schema-free ray values are appended into the final vector instead of first allocating per-ray value arrays. When a loop requests vector-only, schema-free, no-state output, `runtime.js` uses the internal `snapshotTraining()` path instead of the heavier public observation snapshot; returned state still uses public snapshot shapes. `runtime.js` also owns reusable observation scratch for environment results, and `observations.js` uses it to keep controlled-driver car lookup maps and event-grouping containers stable across headless reset/step builds while no-scratch direct calls retain fresh lookup containers. Environment-owned simulations always use the internal track query index because repeated track/surface/ray queries otherwise dominate step cost. `sensors.js` is a compatibility barrel; nearby-car scans, ray defaults, ray config normalization, track rays, surface rays, indexed ray-band intersections, car rays, body senses, contact-patch senses, boundary senses, opponent radar, and ray geometry live under `src/environment/sensors/`. Expensive surface-ray channels are opt-in and appear in the vector schema whenever requested. Replay-ghost sensor type flags are also part of compact ray/nearby-car vectors so vector-only consumers can distinguish real cars from opt-in references. Driver rays use a precision-aware cached origin state, shared per-observation ray target context, and optional per-batch scratch storage for ray-detectable target arrays and target wrappers, rich ray result containers, shared indexed-query objects, filtered car targets, reusable ray-boundary offset/distance/segment-id containers, reusable tracer result containers, validated boundary samples, and bounded sampled-recovery samples. Scratch reuse is internal to environment/render loops; exported sensor calls without scratch continue returning fresh arrays/objects. Driver `precision: 'driver'` road-edge, kerb, and illegal-surface channels use direct indexed ray-band intersections for safe main-track geometry, including track edge, kerb outer edge, gravel edge, and runoff edge. Normal driver candidates are validated against driver-precision track state before they become model-facing values; validated off-track recovery boundaries can also remain on the direct path. The ray-band tracer owns boundary-distance scratch and the returned road-edge/kerb/illegal-surface result shell through the per-ray shared query so direct model-facing rays reuse their offset list, distance list, segment-id list, boundary result object, and channel result objects instead of rebuilding those containers per ray. Pit-lane-origin side rays whose current pit-road frame proves the lateral road boundary resolve directly from pit-road normal/width geometry with no nearest-track sampling; ambiguous recovery, pit-connector, and unproven pit-lane-origin rays stay inside the ray-band tracer as a single bounded sampled-recovery pass for all requested road/surface channels. Debug precision also uses that tracer-owned route with additional sampled refinement for diagnostic distances; only missing-index cases use the older sampled fallback path. The `batch-training` interaction profile now uses that same tracer-owned road/surface path for safe main-track rays, including kerb/illegal-surface-only batches that synthesize same-pose origin state from `progress`/`signedOffset` and pass a main-track-only tracer mode so pit-connector-adjacent training rays stay on direct indexed geometry instead of sampled connector recovery. Destroyed/out-of-race cars keep the active ray schema but short-circuit to miss-valued rays before creating track ray contexts, which prevents terminal cars in batch training from repeatedly querying far-out geometry. Ray precision defaults to the model-facing `driver` contract; `debug` precision is diagnostic-only and must not be silently substituted into policy inputs or model-sense visualization.
112
+ - `rayChannels.js` owns the canonical ray channel list, reusable scalar channel-flag containers, and legal-surface predicate used by the ray-band tracer. Scratch-backed driver rays write requested-channel dispatch into the per-ray shared query instead of allocating channel `Set` instances, and runtime benchmarks require zero channel-set allocations plus stable channel-flag containers on the common direct ray path. Scratch-backed ray batches also cache normalized raw ray option objects and keep ray-option normalization on scalar loops, so rich/vector/append sensor passes reuse the same normalized rays and channel arrays. Scratch-backed car-ray target discovery also keeps the all-target wrapper array and each self-filtered per-car target array stable, using scalar writes rather than callback-driven list rebuilding before the exact footprint checks.
113
+ - `metrics.js` owns reward-neutral per-driver facts such as progress delta, legal progress delta, off-track/severe-cut state, destroyed state, low-speed state, spin/backwards/instability state, lap completion, lap time, and contact count. Contact metrics dedupe the identifiers on each normalized physical contact event so one event increments each involved driver once. `runtime.js` owns reusable metric scratch for environment result builds, and `metrics.js` uses it to keep previous/current car lookup maps plus contact-count grouping containers stable across headless reset/step builds while no-event builds skip contact-count containers. It must not encode or choose rewards.
113
114
  - `recorder.js` owns neutral rollout transition export. It snapshots observations, actions, rewards, next observations, terminal flags, and info at record time and does not interpret rewards or policies.
114
115
  - `controllerLoop.js` owns browser/headless controller orchestration around an injected runtime. It may lazily reset a fresh runtime before scheduled playback, but public `reset()` remains an explicit playback stop. Controller contexts report current `actions` separately from `previousActions`, where previous means the last action map actually applied before the current physics step. It maintains an observation cache so reset-only `resetDrivers()` results can stay small without handing null observations to the next batch decision for non-reset controlled drivers. Scheduled playback catches controller/runtime failures, stops the loop, and exposes the thrown value through loop stats.
116
+ - `local-preview/src/policyRunner/controllers.js` owns the browser Policy Runner HTTP policy-server transport. Its protocol is intentionally compact: reset sends static specs/configuration plus `previousActionFields` and `metricFields` once, while per-decision JSON contains only `driverIds` plus aligned numeric `vectors`, compact previous-action tuples, compact metric tuples, and events. It must not reintroduce full observation objects, schemas, snapshots, or static track metadata into `/policy/decide-batch`; those belong in reset/init metadata or explicit debug/export surfaces.
115
117
  - `evaluation.js` owns deterministic evaluation cases and metrics. It can run a supplied policy against fixed seeds/scenarios, internally requests the minimal snapshot needed when compact no-state base options are supplied, and reuses environment metric semantics for legal surfaces and contacts. It must not update models or define reward objectives.
116
118
  - `workerProtocol.js` owns the JSON-serializable wrapper used by external bridges. It forwards reset/step/spec/state messages to an environment instance, including `getState` output selection, and does not choose Python, Gymnasium, PettingZoo, or storage infrastructure.
117
119
 
@@ -119,7 +121,7 @@ The environment subpath must not import `src/index.js`, package CSS, PixiJS, `F1
119
121
 
120
122
  `src/simulation/rulesConfig.js` is the public rules normalization facade. Defaults live in `src/simulation/rules/ruleDefaults.js`, ruleset presets in `rulesetPresets.js`, merge/value helpers in `ruleConfigMerge.js`, module normalization in `moduleConfig.js`, and penalty consequence normalization in `penaltyConfig.js`. Race modules should be added through those focused files first, then consumed by simulation logic through the normalized `race.rules` object.
121
123
 
122
- `src/simulation/track/trackModel.js` is the canonical track-model facade. Procedural centerline generation, validation, sample rebuilding, start-straight normalization, sectors, DRS zones, pit-lane layout/access/state, spatial query indexing, and spatial queries live in focused `src/simulation/track/*` modules. `src/simulation/trackModel.js` remains a compatibility re-export for tests or older internal imports, but new production code should import the canonical track module. `buildTrackModel()` attaches a non-enumerable internal `track.queryIndex` after samples, DRS, sectors, and pit-lane geometry are finalized, and cached built models freeze their static geometry so consumers cannot mutate shared pit-lane data between builds. Race simulations clone the built track to own mutable timing/pit state and default to attaching a fresh runtime index fork unless `trackQueryIndex: false` is explicitly requested. `trackQueryIndex.js` owns the public indexed-query orchestration over typed centerline segment data, arc-length buckets, surface band thresholds, and deterministic nearest/ray query selection. Generic uniform grid operations live in `trackQueryGrid.js`, scratch buffer/epoch reuse lives in `trackQueryScratch.js`, query diagnostics live in `trackQueryStats.js`, and pit-lane road/box grid construction plus pit candidate lookups live in `trackQueryPitIndex.js`. Public callers still use `nearestTrackState()` and the same snapshot shapes; the index is internal infrastructure and must not leak into JSON snapshots or host contracts. Indexed nearest lookup resolves equal-distance centerline segment ties deterministically inside the index; legacy nearest-sample fallback is reserved for missing indexes, invalid positions, or points outside the indexed world bounds. Ray-boundary queries walk the tight segment grid along the ray corridor instead of querying the expanded recovery grid, which keeps compact training-track profiles from producing thousands of false candidate segments per ray. The model explicitly straightens the start/finish window so the starting grid, pit-entry approach area, and immediate post-line exit area are geometrically straight instead of merely choosing the least-curved generated segment. The pit lane is generated next to that start straight for every built track and exposes entry/exit connectors, explicit procedural road centerlines that overlap the track surface, connect on the lane-facing side of the main track, are tangent to the track at merge points and to the main pit lane at pit-lane endpoints, a straight main fast lane sized from the team/box run plus bounded entry/exit buffers, a parallel working lane, one shared service area per team, queue geometry behind each service area, and two garage boxes per team. `src/rendering/proceduralTrackAsset.js` is now only the Pixi container lifecycle wrapper; main-circuit drawing, pit-lane drawing, grid rendering, finish-line rendering, material constants, render geometry, and offset-stroke safety live under `src/rendering/track/`. `F1SimulatorApp` overlays the dynamic pit-lane status light because open/closed/red-flag state belongs to race control, not the static track texture.
124
+ `src/simulation/track/trackModel.js` is the canonical track-model facade. Procedural centerline generation, validation, sample rebuilding, start-straight normalization, sectors, DRS zones, pit-lane layout/access/state, spatial query indexing, and spatial queries live in focused `src/simulation/track/*` modules. `src/simulation/trackModel.js` remains a compatibility re-export for tests or older internal imports, but new production code should import the canonical track module. `buildTrackModel()` attaches a non-enumerable internal `track.queryIndex` after samples, DRS, sectors, and pit-lane geometry are finalized, and cached built models freeze their static geometry so consumers cannot mutate shared pit-lane data between builds. Race simulations clone the built track to own mutable timing/pit state and always attach a fresh runtime index fork. `trackQueryIndex.js` owns the internal indexed-query orchestration over typed centerline segment data, arc-length buckets, local segment neighborhoods, surface band thresholds, and deterministic nearest/ray query selection. Generic uniform grid operations live in `trackQueryGrid.js`, scratch buffer/epoch reuse lives in `trackQueryScratch.js`, query diagnostics live in `trackQueryStats.js`, and pit-lane road/box grid construction plus pit candidate lookups live in `trackQueryPitIndex.js`. Public callers still use `nearestTrackState()` and the same snapshot shapes; the index is internal infrastructure and must not leak into JSON snapshots or host contracts. `trackSnapshotJson.js` now owns the compact JSON-only track serializer: in-memory snapshots keep the full live track object, while `JSON.stringify(snapshot.track)` reuses a cached rounded public geometry view, decimates exported track samples into a schema-plus-array table, and drops engine-only pit bounds, pit connector bounds, pit-crew metadata, and duplicated pit-box/service-area team fields so repeated full-state transport does not pay to serialize query helpers, repeated object keys, or redundant static pit metadata. Indexed nearest lookup treats progress hints only as optimization bounds, resolves equal-distance centerline segment ties deterministically inside the index, and proves finite-point nearest projections through the tight segment grid before falling back to a deterministic global indexed segment scan only if the grid cannot produce candidates. Low-density cell exact queries are certified by precomputed ring-1 segment neighborhoods plus ring-2 cell lower bounds before arc buckets run, so gravel-adjacent nearest lookups stay on the grid-owned proof path unless the local cell density or lower-bound check requires broader refinement. Outside-runoff nearest lookups use the sparse exact segment-grid walk directly before arc buckets, so far-out recovery and destroyed-car classifications stay on a grid-owned exact proof instead of seeding that same exact result through arc candidates first. Hinted local arc projections now scan deduped arc-bucket candidates directly instead of materializing transient candidate arrays before projection, while local segment-neighborhood projections reuse an already-known centerline segment id to stay on a smaller exact neighborhood for connector and runoff fast paths, write their final projections into reusable target objects instead of allocating a fresh projection result on every exact local query, and solve batched wheel-center neighborhoods through one shared segment scan so connector local-refresh queries do not walk the same exact neighborhood four separate times. Missing indexes are programmer errors on built runtime tracks. Ray-boundary queries walk the tight segment grid along the ray corridor instead of querying the expanded recovery grid, which keeps compact training-track profiles from producing thousands of false candidate segments per ray. The model explicitly straightens the start/finish window so the starting grid, pit-entry approach area, and immediate post-line exit area are geometrically straight instead of merely choosing the least-curved generated segment. The pit lane is generated next to that start straight for every built track and exposes entry/exit connectors, explicit procedural road centerlines that overlap the track surface, connect on the lane-facing side of the main track, are tangent to the track at merge points and to the main pit lane at pit-lane endpoints, a straight main fast lane sized from the team/box run plus bounded entry/exit buffers, a parallel working lane, one shared service area per team, queue geometry behind each service area, two garage boxes per team, and connector-only runtime bounds used to fence pit-aware runoff and query helpers without broadening the whole pit-straight AABB. `src/rendering/proceduralTrackAsset.js` is now only the Pixi container lifecycle wrapper; main-circuit drawing, pit-lane drawing, grid rendering, finish-line rendering, material constants, render geometry, and offset-stroke safety live under `src/rendering/track/`. `F1SimulatorApp` overlays the dynamic pit-lane status light because open/closed/red-flag state belongs to race control, not the static track texture.
123
125
 
124
126
  `src/simulation/rules/` owns focused rule calculation helpers. Each stewarded rule should have its own function and, once non-trivial, its own file. `raceSimulation.js` should orchestrate timing/state and call those helpers; rule modules should return events or penalty payloads rather than mutating the whole simulation directly.
125
127
 
@@ -149,7 +151,7 @@ Responsibilities:
149
151
  - Restart behavior for race data, seed, and track changes. Asset URL changes and browser expert mode changes are intentionally outside restart because texture loading and ticker ownership are part of initialization.
150
152
  - Lifecycle cleanup, including partial-init failure cleanup and destruction of replaced PixiJS display children without destroying shared textures.
151
153
 
152
- `src/app/camera/` owns camera state, camera constants, camera-safe-area measurement, track/pit-lane bounds fitting, zoom limits, mode availability, and target/frame calculation for overview, leader, selected, show-all, and pit cameras.
154
+ `src/app/camera/` owns camera state, camera constants, camera-safe-area measurement, track/pit-lane bounds fitting, zoom limits, mode availability, rotation, and target/frame calculation for overview, leader, selected, driver, show-all, and pit cameras.
153
155
 
154
156
  `src/app/rendering/` owns PixiJS rendering surfaces:
155
157
 
@@ -164,7 +166,7 @@ Responsibilities:
164
166
 
165
167
  `src/app/readouts/` owns generated-DOM readout rendering:
166
168
 
167
- - `timingTowerRenderer.js` owns timing tower rows, gap modes, waved-flag labels, and timing penalty badges.
169
+ - `timingTowerRenderer.js` owns timing tower rows, runtime gap-mode labels/toggle state, waved-flag labels, and timing penalty badges. A single simulator may mount more than one timing tower through standalone and embedded composable surfaces, so timing row rendering fans out through plural timing-list bindings while keeping row markup/caching in this focused renderer. DOM-capable timing lists use keyed row updates that reuse existing `li`/`button` nodes and update only changed text, classes, styles, and penalty badges; non-DOM test stubs keep the string `innerHTML` fallback.
168
170
  - `telemetryRenderer.js` owns selected-car telemetry and lap/sector readouts.
169
171
  - `carOverviewRenderer.js` owns driver/vehicle overview field selection and rendering.
170
172
  - `raceStatusRenderer.js` owns race mode/lap/DRS/contact/start-light/finish readouts.
@@ -184,11 +186,15 @@ Expert sensor visualization belongs to the browser app layer, not the headless e
184
186
 
185
187
  Track material rendering must use the same surface-band thresholds as simulation. `src/rendering/track/trackMaterialRenderer.js` exposes `getTrackMaterialBands(track)` as the rendering-side band contract derived from `track.width`, `track.kerbWidth`, `track.gravelWidth`, `track.runoffWidth`, and `track.barrierWidth`; gravel, runoff, visible barrier walls, ray surface hits, wheel surfaces, and barrier destruction must not invent independent offsets or hardcoded widths. Barrier contact uses the visible vehicle footprint against the rendered wall's inner face so visual contact, destruction, and the environment's terminal state agree.
186
188
 
189
+ `src/simulation/vehicle/vehiclePhysics.js` owns the simulator/arcade integration paths and keeps the per-step physics math allocation-light. The advanced simulator path now resolves forward/right heading axes as direct scalars inside the hot loop instead of allocating a temporary `vehicleAxes()` object tree on every integration step, so velocity projection and acceleration reconstruction stay in the same scalar path as the rest of the physics integration.
190
+
187
191
  Environment sensor target selection belongs to `src/environment/sensors/sensorTargets.js`. It adapts real cars and opt-in replay ghosts into a common sensor target shape while preserving the invariant that replay ghosts stay out of `snapshot.cars` and race systems. Radar enrichment keys targets by entity type plus id, so duplicate ids between real cars and replay ghosts do not cross-contaminate velocity-derived nearby-car facts.
188
192
 
189
193
  `F1SimulatorApp.js` should stay a facade/orchestrator. New camera, rendering, readout, banner, or runtime behavior should usually land in the matching `src/app/*/` ownership module rather than expanding the app class.
190
194
 
191
- `src/app/domBindings.js` owns DOM selector lookup and null-safe readout text writes for package-generated UI surfaces.
195
+ `src/app/domBindings.js` owns DOM selector lookup plus null-safe text and CSS custom-property writes for package-generated UI surfaces. It exposes both singular compatibility handles and plural collections for package-owned readouts that can appear more than once in the same simulator root, such as timing tower rows, lap counters, race-control status slots, and race-data panels. Shared helpers in this module resolve plural collections with singular fallbacks, skip duplicate node work, and avoid repeated identical text/style writes, so focused renderers do not each reimplement that control flow. Race-data panel bindings are grouped at lookup time so banner renderers and controls update embedded lower-thirds and standalone panels without repeated runtime DOM queries. Car/driver overview field label/value nodes are also grouped once at lookup time so overview renders update cached field bindings instead of rediscovering children on every changed overview render.
196
+
197
+ `src/app/layoutSupport.js` owns the package support-envelope observer for browser UI surfaces. It measures generated `data-paddock-component` surfaces and all-in-one shells, sets `data-paddock-size-unsupported` when a shell/mount is narrower than `320px`, when a component is below its component-specific inline minimum, or when a surface is below its minimum usable block size, and leaves CSS to show the accessible package-owned placeholder. Component-specific inline minima matter because package internals such as the broadcast timing tower are intentionally narrower than `320px` and must not self-report unsupported at their valid package-owned width. `src/app/raceOverlayClearanceSupport.js` owns the related narrow race-overlay measurement: it observes responsive embedded timing canvases, checks whether the lower-third would cover timing-row entries in the open reveal geometry, and sets a package CSS variable/class only when extra bottom clearance is required. The lower-third may cover timing-tower chrome/background that has no entries. These observers do not change simulation state, camera math, public API, or host layout; they only record whether the current package-owned surface can render safely.
192
198
 
193
199
  ## Simulation Core
194
200
 
@@ -203,6 +209,8 @@ Responsibilities:
203
209
  - Event buffer lifecycle.
204
210
  - Delegation to feature-owned simulation modules.
205
211
 
212
+ The fixed-step loop is a phase pipeline. Normal active steps read the previously committed order/gap/DRS/rank state, update race control, choose controls, run local car motion, pit routing, runoff, collisions, local surface refresh, stalled-DNF review, and then perform one broad race-state commit. The broad commit is the only normal active-step phase that recalculates race distance, laps, sector timing, timing-line crossings, order, gaps, DRS latch state, finish state, and classification-facing rank fields. Same-lap leader gaps now accumulate from already-computed interval gaps inside that ordered broad-commit loop instead of re-estimating a fresh leader-vs-car timing gap for every same-lap follower, with the direct leader estimate kept only as a fallback when interval timing is not finite. Interval gap estimation itself now bounds timing-line lookups to the overlapping stored line window for the two cars instead of scanning the whole retained line-history span every time, returns before touching timing-line crossings at all when either car has not stored a valid timing-line window yet, and skips the timing-line helper call entirely on that empty-window path so the common broad-commit wrapper falls straight back to timing-history interpolation without extra timing-line setup work. Sector-performance classification is also dirty-gated inside the commit: the simulation still keeps `lapTelemetry.sectorPerformance` exact, but it only rebuilds the field-wide current/last/best sector classification buffers when a sector split or reset changed the underlying sector arrays or when the field-wide overall best sector values changed. This intentionally allows driver decisions to react to the previous committed frame rather than forcing a broad pre-step recalculation. Early-return phases such as grid hold and red flag still leave a committed state before returning, but they do not perform an additional active-step commit.
213
+
206
214
  `src/simulation/race/raceControlState.js` owns initial race-control and safety-car state construction.
207
215
 
208
216
  `src/simulation/race/raceLifecycle.js` owns start-light updates, grid hold/release, and pit-lane open/closed transitions.
@@ -213,15 +221,15 @@ Responsibilities:
213
221
 
214
222
  `src/simulation/race/raceDistance.js` owns pure race-distance utilities including total-lap normalization, progress wrapping, lap calculation, and finish-distance calculation.
215
223
 
216
- `src/simulation/race/raceOrder.js` owns ordered-car context, driver race context, aggression calculation, and DRS reference lookup.
224
+ `src/simulation/race/raceOrder.js` owns ordered-car context, driver race context, aggression calculation, and DRS reference lookup. The normal running path now partitions participating cars into live and DNF sets once before sorting, keeps the common running path free of per-call `id -> car` maps unless a finished/classification/frozen-order branch actually needs that lookup, and sorts the live field in place before appending DNF cars instead of cloning and reassembling extra arrays on the hot running branch. Broad race-state commits should derive nearest-ahead DRS references from one physical-track-order pass over the current active field instead of re-scanning the whole field per car. When the active running field still fits inside one lap window, the DRS helper may safely reuse the already ordered race field as that physical order and skip the extra wrapped-progress sort altogether; once lapped traffic appears, it falls back to the explicit physical-order sort so DRS can still reference physically ahead backmarkers correctly.
217
225
 
218
226
  `src/simulation/participants/participantInteractions.js` owns the resolved per-car interaction contract. It normalizes profiles and overrides, attaches `car.interaction` during race setup, and provides helpers for collision, sensor visibility, pit-lane blocking, and race-order membership. It does not own vehicle motion or policy behavior.
219
227
 
220
228
  `src/simulation/replay/replayGhosts.js` owns trajectory-driven replay/reference entities. It normalizes ghost input, advances ghost state from race time, interpolates trajectory samples, and serializes `snapshot.replayGhosts`. Replay ghosts are not physics participants and must remain outside car collisions, pit systems, timing/classification, stewarding, and car hit areas.
221
229
 
222
- `src/simulation/race/raceProgress.js` owns per-step race-state recalculation: progress, lap telemetry, sector timing, gaps, rank fields, attack state, and DRS latch coordination.
230
+ `src/simulation/race/raceProgress.js` owns local surface refresh and broad race-state commit helpers. Local refresh updates current wheel/track state needed by post-motion rule phases without changing timing/order. When runoff has already computed a center track state for the same final pose, local refresh may reuse it; if collision correction changed the pose, it discards that pending state and queries from the final position. Connector-adjacent wheel refresh now keeps those reused center states on an exact local segment-neighborhood path for wheel-center classification, instead of falling back to broad nearest-track scans at each wheel. Normal active steps now treat that local refresh as the authoritative post-motion surface pass, so the following broad commit reuses the already-committed wheel/track state instead of recomputing the same surface classification a second time for every car. The broad commit also derives DRS references from a one-pass physical track ordering once per commit instead of calling back into per-car whole-field DRS scans, keeps those references in a reusable per-car indexed scratch array rather than rebuilding a transient `Map` just to feed the ordered commit loop, and now skips that extra physical-order sort entirely when the ordered live field is provably still inside one lap window. DRS latch updates also cache the currently active zone reference on the car, cache the next detection-zone index across steady forward motion, mutate zone detection records in place, and stay on a sim-bound scalar call path during broad commits so the hot loop avoids rebuilding the same options object for every car on every step. External car-state changes clear that next-zone cache so repositioned cars still detect later zones correctly. It passes one precomputed live field depth into aggression updates instead of re-filtering the full field once per car, skips the excluded-participant reset scan entirely when the ordered list already covers every car in the field, and threads that same ordered field into finish evaluation instead of recomputing race order again at the end of the commit. On the common non-finish path, that finish evaluation now stays on one direct ordered-field pass instead of allocating a temporary `newlyFinished` array and then rescanning the field again just to prove the race is still running. Timing sample recording and timing-line crossing updates are folded into that same per-car commit loop, so the normal step no longer pays a second whole-field timing pass immediately after recomputing progress and lap telemetry. Lap telemetry and sector-performance helpers now keep their fixed three-sector buffers hot in place during normal broad commits, and lap-telemetry position refresh rewrites the existing `lapTelemetry` fields in place instead of allocating a throwaway lap/sector position object on every per-car update. In the dominant no-boundary case, lap telemetry now stays on an even narrower in-progress sync path that updates only the live scalars and the active sector slots instead of re-synchronizing the whole three-sector position structure as if a split had happened. Timing-gap estimation still limits timing-line scans to the overlapping stored window shared by the compared cars instead of walking the whole retained timing-line range on every gap estimate, and it now returns immediately when either compared car has not stored any usable timing-line window yet. Inside `src/simulation/vehicle/wheelSurface.js`, the hot `applyWheelSurfaceState()` path now lets `calculateWheelSurfaceState()` write the resolved representative state straight into the stable `car.trackState` container, instead of building a temporary representative object and immediately copying it back into that same hot-loop target. The broad commit owns progress, lap telemetry, sector timing, timing-line crossings, gaps, rank fields, attack state, DRS latch coordination, and finish evaluation.
223
231
 
224
- `src/simulation/race/raceFinish.js` owns finish detection and race-finish transitions while classification ranking remains in `classification.js`.
232
+ `src/simulation/race/raceFinish.js` owns finish detection and race-finish transitions while classification ranking remains in `classification.js`. The common no-finish path should stay allocation-light: it may mark newly finished cars during one ordered-field pass, but it should not build throwaway arrays or do a second full-field confirmation scan before returning to the running race.
225
233
 
226
234
  `src/simulation/race/classification.js` owns final classification ranking and classification consequences.
227
235
 
@@ -235,7 +243,7 @@ Responsibilities:
235
243
 
236
244
  `src/simulation/pit/pitFlow.js` owns pit-stop flow orchestration: scheduling, rearming, entry eligibility, stop start, queue/service/exit progression, and delegation to pit routing/service helpers.
237
245
 
238
- `src/simulation/pit/pitRouting.js` owns pit-route construction, pit-lane route sampling, limiter-segment lookup, and render-pose shifts used by route/contact movement.
246
+ `src/simulation/pit/pitRouting.js` owns pit-route construction, pit-lane route sampling, limiter-segment lookup, nearest-route projection, and render-pose shifts used by route/contact movement. Internal pit-route movement samples into the pit stop's reusable route point and projects against route-owned scratch so entry, queue-release, and exit transitions stay allocation-light; the compatibility `sampleRoute()` helper remains object-returning for no-scratch callers.
239
247
 
240
248
  `src/simulation/pit/pitService.js` is the public pit-service export surface and owns only the service-entry handoff. Occupancy, queue behavior, service profile timing, penalty service, tire service, pit-exit release, and pit-route movement are split across focused `src/simulation/pit/pit*.js` modules.
241
249
 
@@ -247,27 +255,31 @@ Responsibilities:
247
255
 
248
256
  `src/simulation/rules/penaltyStats.js` owns penalty-stat aggregation and lookup helpers.
249
257
 
250
- `src/simulation/vehicle/contactResolution.js` owns physical collision resolution and contact velocity response. Stewarding remains separate from physical response. In simulator mode, every collision branch that changes speed also keeps `velocityX/Y` synchronized, including contacts where one car is fixed by pit entry, queue, service, or exit routing.
258
+ `src/simulation/vehicle/contactResolution.js` owns physical collision resolution and contact velocity response. Stewarding remains separate from physical response. In advanced mode, every collision branch that changes speed also keeps `velocityX/Y` synchronized, including contacts where one car is fixed by pit entry, queue, service, or exit routing. The hot response path stays on scalar velocity components rather than rebuilding temporary vector objects for impulse and damping math. The resolver keeps phase-level collidable-car, reported-contact, and steward-context containers in `sim.collisionScratch`, so collision phases reuse their orchestration scratch before entering candidate generation, narrowphase checks, and fresh-contact penalty review. `src/simulation/rules/rulesReview.js` writes collision steward facts into that scratch-owned context when provided and emits resulting collision penalties directly into the simulation ledger, rather than building an intermediate penalty array for the internal synchronous review path. Direct/no-scratch review calls still receive an isolated context for the synchronous penalty calculation.
251
259
 
252
- `src/simulation/vehicle/runoffResponse.js` owns non-penalty off-track vehicle correction.
260
+ `src/simulation/vehicle/runoffResponse.js` owns non-penalty off-track vehicle correction. It checks the main-track barrier boundary and records the center track state it already computed, but normal running does not apply wheel-surface state there; post-motion local refresh owns the final wheel/contact-patch classification. Normal cars now reach that barrier check through a local segment-neighborhood projection derived from the previously committed track frame when pit geometry cannot apply, so runoff stays on a geometry-owned fast path instead of paying a full indexed nearest-track query or broader hinted-arc scan every step. That fast path now uses the tightest exact neighborhood only for small committed-step motion, then widens back to the broader exact local neighborhood before any teleport fallback, which keeps common step-sized runoff queries cheap without loosening the exact track-state contract. Its main-track state and returned result wrapper are reused per car across steps, so the hot runoff path stops allocating a fresh exact-query track-state container on every frame. Pit-lane, pit-bound, and teleport-like moves still fall back to the full indexed/pit-aware query path. Barrier reach against that main-track state is computed analytically from the body, wheels, and visual footprint instead of re-projecting every corner each step.
253
261
 
254
262
  `src/simulation/snapshots/raceSnapshots.js` owns public, render, and observation snapshot assembly.
255
263
 
256
- Weather effects, reliability failures, and fuel-load performance effects are not active `2.0.x` behavior. Their future ownership should stay separate from the race orchestrator: weather belongs in a future `src/simulation/weather/` module that returns track/session condition modifiers, reliability belongs in a future `src/simulation/reliability/` module that emits deterministic failure/degradation events, and fuel-load effects belong in a future vehicle-adjacent performance module that adjusts mass/pace through explicit inputs rather than hidden mutation.
264
+ Weather effects, reliability failures, and fuel-load performance effects are not active simulator behavior. Their future ownership should stay separate from the race orchestrator: weather belongs in a future `src/simulation/weather/` module that returns track/session condition modifiers, reliability belongs in a future `src/simulation/reliability/` module that emits deterministic failure/degradation events, and fuel-load effects belong in a future vehicle-adjacent performance module that adjusts mass/pace through explicit inputs rather than hidden mutation.
257
265
 
258
- `snapshot()` is the full public data contract and must remain stable for host callbacks, environment state, and external callers. Internal browser/rendering paths use lean read models such as `snapshotRender()` so the 60 FPS loop does not serialize setup, wheels, penalties, and other non-render fields every frame. Environment and sensor code may use similarly narrow internal views, cache shared per-observation ray origin state, and broadphase car-ray checks before exact footprint intersection, but those must not change the public `RaceSnapshot` shape.
266
+ `snapshot()` is the full public data contract and must remain stable for host callbacks, environment state, and external callers. Returning a full snapshot object is separate from repeatedly JSON-serializing it: full JSON serialization is reserved for explicit host/export/debug paths, worker/full-state requests, tests, and benchmarks. Internal browser/rendering paths use lean read models such as `snapshotRender()` so the 60 FPS loop does not serialize setup, wheels, penalties, and other non-render fields every frame. Environment and sensor code may use similarly narrow internal views, cache shared per-observation ray origin state, reuse scratch-backed rich ray channel containers, and broadphase car-ray checks before exact scalar footprint intersection using the caller-owned ray vector, but those must not change the public `RaceSnapshot` shape or the fresh-object behavior of no-scratch public ray calls.
259
267
 
260
268
  `src/simulation/driverController.js` is a compatibility export for AI control decisions; canonical driver policy code lives under `src/simulation/driver/`. The dispatcher selects green-flag, rejoin, and safety-car controls, while separate modules own input building, personality generation, traffic scanning, edge recovery, edge-motion analysis, rejoin behavior, safety-car queueing, and racing-line attack/defense planning. It does not move cars directly; it returns steering/throttle/brake controls for `vehiclePhysics.js` to integrate.
261
269
 
262
- `src/simulation/vehicle/vehicleGeometry.js` owns deterministic car footprint math. It exposes the rendered-scale body hull, four wheel/contact-patch oriented rectangles, current/previous pose generation, AABB helpers, and a per-car `geometryState` cache keyed by current and previous pose. Collision detection, wheel-surface sampling, snapshots, and debug previews consume this module instead of each recreating car dimensions. Root `src/simulation/vehicleGeometry.js`, `vehiclePhysics.js`, and `wheelSurface.js` are compatibility re-exports.
270
+ `src/simulation/vehicle/vehicleGeometry.js` owns deterministic car footprint math. It exposes the rendered-scale body hull, four wheel/contact-patch oriented rectangles, current/previous pose generation, AABB helpers, and a per-car `geometryState` cache keyed by current and previous pose. Collision detection, wheel-surface sampling, snapshots, and debug previews consume this module instead of each recreating car dimensions. Its current-pose geometry writer now keeps body/wheel containers stable while also writing body and wheel rectangles directly from shared heading scalars, so post-motion local refresh no longer allocates temporary axis/center objects just to rebuild the same cached geometry tree. On that current-pose cache, wheel and body corners now stay lazy by default and are only materialized when a sampled wheel-surface path actually needs them; once materialized they are updated in place and reused across pose changes. The hot current-only cache also drops the old string signature bookkeeping, because runtime consumers match pose through the cached pose object itself rather than through rebuilt `x:y:heading` strings. Root `src/simulation/vehicleGeometry.js`, `vehiclePhysics.js`, and `wheelSurface.js` are compatibility re-exports.
263
271
 
264
- `src/simulation/collisionGeometry.js` owns shape-confirmed vehicle contact. It first prunes pairs by nearby track progress with a circular-track window, then uses swept body AABBs, SAT body/body overlap checks, and conservative continuous checks between previous and current poses. Wheel shapes are intentionally excluded from car-vs-car collision for cost and stability; wheel geometry is still used by the surface and track-limit pipeline. `raceSimulation.js` only orchestrates response/cooldown/steward review after this module returns contact metadata, and collision stewarding derives rear-contact fault from physical track order so lapped cars are handled consistently.
272
+ `src/simulation/collisionGeometry.js` owns shape-confirmed vehicle contact. It first prunes pairs by nearby track progress with a circular-track window, then uses swept body AABBs, SAT body/body overlap checks, and conservative continuous checks between previous and current poses. Hot internal broadphase calls reuse scratch-owned candidate arrays, pooled pair arrays, source-indexed distance entries, typed pair marks for candidate de-duplication, and reusable missing-distance flag/index arrays; public no-scratch candidate queries still return fresh pair arrays. Candidate generation carries each sorted distance entry's source car index directly into hidden pair metadata and the pair marker, so the hot path no longer builds an object `Map` for car order or a fallback `Set` for missing-distance membership. The swept narrowphase now interpolates body-only shapes into reusable scratch rectangles instead of rebuilding full temporary vehicle geometry on every sweep sample. Hot internal collision calls also keep shape-contact results, vehicle-contact metadata, and their contact-axis objects in reusable scratch containers; public no-scratch collision queries still return fresh result objects. The simulation collision resolver uses the hidden candidate-pair indexes to mark already-reported fresh contacts in a scratch-owned typed epoch buffer instead of building string keys in a `Set` during the collision phase. Wheel shapes are intentionally excluded from car-vs-car collision for cost and stability; wheel geometry is still used by the surface and track-limit pipeline. `raceSimulation.js` only orchestrates response/cooldown/steward review after this module returns contact metadata, and collision stewarding derives rear-contact fault from physical track order so lapped cars are handled consistently.
265
273
 
266
- `src/simulation/vehicle/wheelSurface.js` owns per-wheel track sampling and the effective car surface. Surface priority, analytic main-track classification, and pit-lane/connector classification are split into `surfacePriority.js`, `mainTrackWheelSurface.js`, and `pitWheelSurface.js`. On normal main-track running it uses the cached wheel geometry and the car center's track normal to analytically classify each wheel patch from signed offsets. It falls back to full patch sampling near pit connectors and inside pit-lane states. `vehiclePhysics.js` consumes that effective surface through the car track state, and separately compares left-side and right-side wheel resistance to add a small capped yaw bias toward the slower side.
274
+ `src/simulation/track/spatialQueries.js`, `pitLaneState.js`, and the pit sections of `trackQueryPitIndex.js` own drivable-region state resolution. Main-track nearest projection stays indexed through the centerline query index, while pit-road and pit-box candidate lookup stays inside the pit query index. The pit query helpers can now reuse scratch candidate arrays/route buckets for hot internal callers such as `nearestTrackState()`, so pit entry/fast-lane/working-lane/exit and pit-box classification no longer rebuild temporary candidate containers on every call. Indexed pit-road classification also projects restricted entry/fast-lane/working-lane/exit candidate segments into one reusable polyline result while reading cumulative route distances precomputed on the pit query index, instead of rebuilding candidate sets, filtered segment arrays, and distance arrays per route. Connector-adjacent direct pit-entry/pit-exit override checks use that same query-index-owned polyline projection target and route metadata. Endpoint-progress shortcuts read precomputed route endpoint segment windows, while mid-connector points without an endpoint progress hint read bounded route candidates from the pit index without becoming full pit-lane queries, so connector shortcuts avoid full-route pit-entry/pit-exit projection scans and transient projection objects. Direct sensor-ray boundary tracing also asks the query index for reusable segment IDs and then reads typed centerline arrays directly, leaving temporary segment-object materialization to the public/debug `queryTrackSegmentsAlongRay()` helper instead of the hot ray-band tracer. `pitLaneState.js` also skips pit-box grid lookups entirely when a point is outside the dedicated box/service-area bounds, which removes wasted garage/service misses from connector-adjacent wheel and track classification. Transition resolution still chooses between indexed main-track and indexed pit-lane states, but the benchmark gate now explicitly exercises pit queries and requires direct connector projection scratch reuse, positive endpoint-window connector projection calls, zero direct connector full-route projection scans, pit-road grid hits, reused pit-road projection scratch, precomputed pit-road route distances, zero cumulative-distance rebuilds, zero road-grid misses, and `pitFallbacks: 0`. `trackStatePolicy.js` also owns the car-specific runoff fast path: when the previous committed track frame is valid and pit geometry is irrelevant, it resolves the new main-track frame through an exact local segment-neighborhood projection instead of the broader nearest-track search or hinted-arc path, using the tightest exact radius only for roughly meter-scale committed motion and widening back to the broader exact neighborhood automatically when motion is larger. That single-car runoff path now writes its exact local-neighborhood result into a reusable per-car track-state target and reuses a stable result wrapper, so the common post-physics runoff query avoids per-step track-state allocation without changing the exact geometric contract. Pit-aware runoff now only exits that fast path when the previous committed state is already beyond the main-road edge on the pit-lane side and the new pose lands inside connector-only runtime bounds, so normal on-track cars near the pit straight keep the exact local query path while true pit-transition edge cases still fall back to the full pit-aware nearest resolution.
267
275
 
268
- `src/simulation/vehicle/vehiclePhysics.js` owns vehicle integration using the already-resolved wheel surfaces and wheel-state resistance imbalance. It exposes the normalized `physicsMode` contract and routes between the compatibility arcade integrator and the opt-in simulator integrator. The simulator path owns 2D velocity integration, yaw response, traction-budget calculation, speed-sensitive steering, steering scrub, aero drag/downforce balance, per-wheel surface averaging, derived slip-angle telemetry, simulator surface coefficients, and telemetry fields; steering scrub opposes signed forward motion so reverse slides do not gain energy from steering loss. Driver AI and environment policies still provide only steering/throttle/brake/pit intent.
276
+ `src/simulation/vehicle/wheelSurface.js` owns per-wheel track sampling and the effective car surface. Surface priority, analytic main-track classification, and pit-lane/connector classification are split into `surfacePriority.js`, `mainTrackWheelSurface.js`, and `pitWheelSurface.js`. On normal main-track running it uses the cached wheel geometry and the car center's track normal to analytically classify each wheel patch from signed offsets. That normal-running apply path now shares the projected wheel half-width across the four same-orientation patches and folds analytic wheel writes plus surface/track-limit summarization into one pass, so the hot local-refresh path stops recomputing the same projection term and rewalking the same four wheels just to summarize them. The dominant common case now short-circuits directly to that analytic branch whenever the resolved center state is on the main track and not near a pit connector, instead of first walking the pit/connector decision tree before landing on the same result. Near pit connectors it still resolves ambiguous wheel centers through exact local segment-neighborhood projections anchored on the already-known center track segment, but those wheel-center projections now use a tighter exact radius for the real connector cases, write into reusable projection containers, and can be queried as one shared batch instead of rebuilding the same neighborhood search separately for each wheel. Straight connector-adjacent main-track poses first prove the full wheel footprint against the center track frame and skip those wheel-center projections entirely when every patch remains inside the main-track limits, so centered pit-straight connector refreshes use the same analytic writes as normal main-track running. When an exact batch is needed and proves every wheel center still belongs to the main track, the post-query apply path now stays on a one-pass analytic summary instead of routing each wheel back through the slower mixed connector branch plus a second summary pass. Connector-adjacent main-track cars whose full wheel footprint stays on the opposite half of the track from the pit-lane side skip those connector track queries entirely and stay on the plain analytic main-track path, because pit overlap is geometrically impossible in that region. Indexed pit geometry or full patch samples still run only when that local geometric pass says the wheel is genuinely ambiguous against pit-road bounds. Center-in-pit-lane states still use pit analytics when the whole patch fits inside the road and fall back to full sampling otherwise. The internal `applyWheelSurfaceState()` path keeps per-car scratch storage for wheel arrays, wheel objects, sampled-state arrays, reusable connector wheel-center states, reusable connector wheel-center projections, reusable connector wheel-center point refs, a reusable wheel-summary accumulator, a stable mutable `car.trackState`, and a stable cache wrapper so the post-physics refresh path avoids replacing hot-loop containers when only the field values changed; it now also passes the reusable queried-state target array straight through the exact connector batch query instead of allocating a fresh state array and copying the results back out. On cache misses it threads the already-resolved current geometry straight through `calculateWheelSurfaceState()` instead of rereading the same current-geometry cache a second time. Its cache-hit validation now compares geometry pose and explicit center-state fields directly instead of rebuilding per-call cache signature strings, so repeated explicit-center refreshes stay on the cheap cache-hit path without avoidable string churn. It also reads current-pose vehicle geometry from a current-only cache so pre-physics wheel-surface checks do not rebuild wheel geometry just because swept-collision history (`previousX/Y/Heading`) was refreshed, and that current-only geometry cache now updates its body/wheel containers in place when the current pose changes while leaving wheel/body corners lazy until a sampled path requests them, so post-physics local refresh stops rebuilding unused current-geometry corner trees every step without weakening exact sampled wheel checks. The final wheel summary now stays in the classifier pass for scratch-backed main-track, connector, pit-analytic, and full-sample branches, and its reusable `trackLimits` object is the car's stable `trackLimitState`; runtime benchmarks require the summary container, track-limit object, wheel containers, sampled-state objects, and representative track-state object to stay reused. Direct calculation calls still return fresh public containers, but they now use the same scalar wheel writer batches and direct full-patch sampler functions instead of rebuilding callback-driven patch-sample arrays; the wheel-surface benchmark requires scratchless scalar writer batches and one scalar wheel write per wheel on the direct connector path. `vehiclePhysics.js` consumes that effective surface through the car track state, and separately compares left-side and right-side wheel resistance to add a small capped yaw bias toward the slower side.
269
277
 
270
- `src/simulation/vehicle/vehicleState.js` owns external state mutation from simulator and environment reset APIs. It rejects non-finite numeric partial state and resynchronizes simulator velocity from speed/heading when callers do not provide an explicit velocity vector, so scenario placement and `resetDrivers()` cannot leave stale physics velocity behind.
278
+ Timing maintenance is also kept allocation-light now. Runtime timing writes in `timingHistory.js` use a fixed structured ring store for per-car `{ time, raceDistance }` samples, so broad commits no longer allocate a fresh object for every timing sample and long-running races stay bounded without repeated array churn. Structured timing-history interpolation also caches the last successful neighborhood and probes that bracket first on the next nearby lookup, which matches the normal adjacent-car broad-commit pattern and avoids repeatedly rescanning the same trailing timing-history window from the tail. `interpolateTimeAtDistance()` still accepts the older array-of-objects shape so characterization tests and legacy internal fixtures can keep using explicit sample arrays where needed. `timingLines.js` likewise caches the first and last retained mini-sector line numbers, so pruning old timing-line crossings only walks the numeric range when the pruning cutoff actually advances instead of rescanning object keys every step, and zero-travel updates with an unchanged cutoff now stop at the timestamp refresh instead of rewalking stored line keys.
279
+
280
+ `src/simulation/vehicle/vehiclePhysics.js` owns vehicle integration using the already-resolved wheel surfaces and wheel-state resistance imbalance. It exposes the normalized `physicsMode` contract and routes between the compatibility arcade integrator and the opt-in advanced integrator. The advanced path owns 2D velocity integration, yaw response, traction-budget calculation, speed-sensitive steering, steering scrub, aero drag/downforce balance, per-wheel surface averaging, derived slip-angle telemetry, advanced surface coefficients, and telemetry fields; steering scrub opposes signed forward motion so reverse slides do not gain energy from steering loss. Its hot wheel-surface aggregation and left/right drag-yaw resistance paths now stay on direct scalar loops instead of allocation-heavy `filter()` / `reduce()` passes, so advanced integration uses the existing wheel-state contract without paying per-step array churn. Driver AI and environment policies still provide only steering/throttle/brake/pit intent.
281
+
282
+ `src/simulation/vehicle/vehicleState.js` owns external state mutation from simulator and environment reset APIs. It rejects non-finite numeric partial state and resynchronizes advanced-mode velocity from speed/heading when callers do not provide an explicit velocity vector, so scenario placement and `resetDrivers()` cannot leave stale physics velocity behind.
271
283
 
272
284
  `src/simulation/rules/pitLaneSpeedingSteward.js` owns pit-speed-limit review. `raceSimulation.js` calls it after authoritative race-state recalculation so it reads the same pit-lane part classification as snapshots and track-limit stewarding. The steward only checks speed-limited pit-lane parts (`fast-lane`, `working-lane`, service areas, and garage boxes), leaving pit-entry and pit-exit connector roads legal but not speed-limited.
273
285
 
@@ -275,9 +287,9 @@ Browser mounts generate a fresh procedural seed when `trackSeed` is omitted; exp
275
287
 
276
288
  `src/simulation/units.js` owns conversion between simulator units and public meter/km/h display values. Physics stays in simulator units; snapshots expose calibrated display fields such as `speedKph`, `distanceMeters`, and `gapMeters`.
277
289
 
278
- `src/rendering/renderSnapshot.js` owns interpolation for rendering. It accepts either the full public snapshot or the lean render snapshot; render-only callers should prefer the lean snapshot.
290
+ `src/rendering/renderSnapshot.js` owns interpolation for rendering. It accepts either the full public snapshot or the lean render snapshot; render-only callers should prefer the lean snapshot. Browser frame paths may fill reusable render-source and interpolation buffers, while public snapshot methods continue returning fresh caller-owned snapshot objects.
279
291
 
280
- The app runtime pauses its PixiJS ticker when the race canvas is outside the viewport or the document is hidden, then resets the frame clock before resuming. This prevents host pages with several simulator embeds from running every race while only one is visible, and avoids a large simulation catch-up step when the canvas re-enters view. Camera safe-area layout measurements are cached and invalidated by resize observation so the 60 FPS render path does not force repeated DOM geometry reads. Camera framing derives a cached active-track bounds box from generated samples plus pit-lane extent; overview uses that frame directly, and every camera uses it as the zoom-out floor. The first camera frame is applied immediately, then later camera mode changes interpolate target position and scale so leader/selected/overview/show-all/pit switches do not jump the world view. Runtime readouts avoid redundant DOM writes for unchanged text, timing markup, finish classification, and static selected-car overview data, and `updateDom()` reuses its supplied full snapshot for camera-control availability instead of taking another public snapshot. Static pit-lane status graphics are redrawn only when their status/geometry key changes, and DRS trail histories are pruned in place. Startup marks the initial DOM refresh time so the first ticker frame does not immediately redo the same full readout update. At `5x` and `10x` browser playback, noncritical DOM/timing refreshes are throttled more aggressively while fixed-step simulation, event emission, and rendering continue from authoritative simulation state. Long project-radio delays are also treated as stale schedule state instead of replaying every missed lower-third transition.
292
+ The app runtime pauses its PixiJS ticker when the race canvas is outside the viewport or the document is hidden, then resets the frame clock before resuming. Runtime visibility uses `IntersectionObserver` plus a bounded canvas-geometry check on observer, scroll, resize, focus, and page-show events so a visible canvas cannot remain paused because of a stale observer result. Ticker synchronization also compares the intended runtime state with PixiJS' actual `ticker.started` state before deciding whether to call `start()` or `stop()`. This prevents host pages with several simulator embeds from running every race while only one is visible, and avoids a large simulation catch-up step when the canvas re-enters view. Camera safe-area layout measurements are cached and invalidated by resize observation so the 60 FPS render path does not force repeated DOM geometry reads. Camera framing derives a cached active-track bounds box from generated samples plus pit-lane extent; overview uses that frame directly, and every camera uses it as the zoom-out floor. The first camera frame is applied immediately, then later camera mode changes interpolate target position and scale so leader/selected/overview/show-all/pit switches do not jump the world view. Runtime readouts avoid redundant DOM writes for unchanged text, timing markup, finish classification, and static selected-car overview data, and `updateDom()` reuses its supplied full snapshot for camera-control availability instead of taking another public snapshot. Static pit-lane status graphics are redrawn only when their status/geometry key changes, and DRS trail histories are pruned in place. Startup marks the initial DOM refresh time so the first ticker frame does not immediately redo the same full readout update. At `5x` and `10x` browser playback, noncritical DOM/timing refreshes are throttled more aggressively while fixed-step simulation, event emission, and rendering continue from authoritative simulation state. Long project-radio delays are also treated as stale schedule state instead of replaying every missed lower-third transition.
281
293
 
282
294
  ## Data Layer
283
295
 
@@ -305,6 +317,8 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
305
317
  - Race telemetry drawer template.
306
318
  - Car/driver overview panel.
307
319
 
320
+ The package-owned component inventory is maintained in [Component Inventory](component_inventory.md). Use it before changing templates, DOM bindings, readout renderers, layout observers, or local-preview component usage so shared component behavior stays in focused helpers instead of drifting across all-in-one, composable, and derived mixed variants.
321
+
308
322
  `src/ui/bannerTemplates.js` owns package banner markup:
309
323
 
310
324
  - Top steward message for warnings and penalties.
@@ -314,11 +328,11 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
314
328
 
315
329
  `src/ui/templateUtils.js` owns shared template helpers such as escaping and loading-state markup. Banner templates and non-banner component templates should share helpers through this module instead of importing from each other.
316
330
 
317
- `src/ui/shellTemplate.js` composes those component templates into the default all-in-one simulator DOM and package-owned layout presets such as `left-tower-overlay`. Telemetry graph/table surfaces are independent package-owned components controlled by `ui.telemetryModules` when used through stack/drawer templates; app readout modules render them from `car.lapTelemetry` without requiring host-owned DOM. The telemetry stack can embed the car/driver overview, owns vertical scrolling when the host constrains its height, and is also embedded by the hide/show race telemetry drawer. The overview is also a separately mountable component. The project and radio lower-thirds share the same race-data panel markup; runtime state only swaps content and mode classes. The optional project telemetry detail lives inside that shared lower-third, while the broadcast sector banner remains a separate lower-third-style sector graph that can be mounted standalone or embedded inside the race canvas only when a host explicitly asks for that independent popup. Readout modules bind the selected car name/code/color to it through the same readout path as the other telemetry surfaces. The race telemetry drawer template composes an external top control row, race canvas with embedded timing tower, project/radio lower-third, top steward message, safety-car control, and the telemetry stack in a right drawer. Its open/close state is owned by `F1SimulatorApp`; the safety-car control, project/radio banner mute toggle, and telemetry toggle stay in the top row while telemetry is open. The drawer reserves final race-view space with a stable margin while the sidebar itself animates with a compositor transform, avoiding grid-template reflow during the slide. The drawer race area uses the configured workbench height so host embeds do not leave unused black space below the simulation. The left-tower overlay preset is responsible for internal component placement and package-owned proportions; the project/radio lower-third remains owned by the race canvas so it can either use the race space beside the timing tower in `auto` sizing mode or overlap the tower when space is constrained. Composable hosts can also ask the race-canvas template to embed the timing tower directly with `includeTimingTower`, using the same expand-vs-scroll vertical fit contract. `src/app/camera/` measures the resulting timing-tower gutter when framing the PixiJS camera, whether the tower comes from the prebuilt shell or from an embedded composable race canvas. It also owns camera mode availability, wheel/button zoom for every camera mode, hiding the pit camera control when the active track has no pit-lane geometry, and fitting the pit camera from operational `track.pitLane` lane, box, service, and queue bounds instead of host-provided coordinates or long access-road endpoints. On narrow hosts, CSS stacks timing boards full-width and the camera safe-area measurement treats those boards as stacked content instead of side gutters. Hosts should not provide the internal simulator markup or tune preset internals with raw sizing options.
331
+ `src/ui/shellTemplate.js` composes those component templates into the default all-in-one simulator DOM and package-owned layout presets such as `left-tower-overlay`. Telemetry graph/table surfaces are independent package-owned components controlled by `ui.telemetryModules` when used through stack/drawer templates; app readout modules render them from `car.lapTelemetry` without requiring host-owned DOM. The telemetry stack can embed the car/driver overview, owns vertical scrolling when the host constrains its height, and is also embedded by the hide/show race telemetry drawer. The overview is also a separately mountable component. The project and radio lower-thirds share the same race-data panel markup; runtime state only swaps content and mode classes. The optional project telemetry detail lives inside that shared lower-third, while the broadcast sector banner remains a separate lower-third-style sector graph that can be mounted standalone or embedded inside the race canvas only when a host explicitly asks for that independent popup. Readout modules bind the selected car name/code/color to it through the same readout path as the other telemetry surfaces. The race telemetry drawer template composes an external top control row, race canvas with embedded timing tower, project/radio lower-third, top steward message, safety-car control, and the telemetry stack in a drawer. Its open/close state is owned by `F1SimulatorApp`; the safety-car control, project/radio banner mute toggle, and telemetry toggle stay in the top row while telemetry is open. Wide drawer layouts reserve final race-view space with a stable margin while the sidebar itself animates with a compositor transform; responsive narrow drawer layouts are enabled by default, stack the top workbench controls by container width, and reveal over the race view so mobile canvas height does not permanently collapse. The drawer race area uses the configured workbench height so host embeds do not leave unused black space below the simulation. The left-tower overlay preset is responsible for internal component placement and package-owned proportions; the project/radio lower-third remains owned by the race canvas so it can either use the race space beside the timing tower in `auto` sizing mode or overlap the tower when space is constrained. Composable hosts can also ask the race-canvas template to embed the timing tower directly with `includeTimingTower`, using the same expand-vs-scroll vertical fit contract on wide containers and a package-owned left reveal panel on narrow containers. In responsive narrow mode, the race-canvas min-height is governed by the timing tower/race-view minimum until runtime measurement shows the lower-third would cover one or more timing rows; only then does CSS receive the measured clearance needed to move the banner below the entries. `F1SimulatorApp` owns the timing reveal state, keeps closed narrow timing panels inert/hidden, installs the overlay-clearance observer, and resizes the renderer/camera during open, close, and host resize transitions without remounting the simulator. `src/app/layoutScheduler.js` owns the shared RAF coalescing/cancellation primitive used by layout support and overlay-clearance measurement so resize and mutation observers do not duplicate scheduling state. `ui.responsiveNarrowLayout` and matching composable mount options only toggle package-owned narrow template classes; they do not change simulation state or require host-owned layout patches. `src/app/camera/` measures the resulting timing-tower gutter when framing the PixiJS camera, whether the tower comes from the prebuilt shell or from an embedded composable race canvas. It also owns camera mode availability, wheel/button zoom for every camera mode, opt-in driver-camera rotation, hiding the pit camera control when the active track has no pit-lane geometry, and fitting the pit camera from operational `track.pitLane` lane, box, service, and queue bounds instead of host-provided coordinates or long access-road endpoints. On narrow hosts, closed timing reveals are outside the canvas safe area and open reveals are measured continuously by resize observation. Hosts should not provide the internal simulator markup or tune preset internals with raw sizing options.
318
332
 
319
333
  Penalty UI is display-only. `src/app/banners/stewardMessageController.js` may show track-limit warning events and `snapshot.penalties` in the top steward message, and `src/app/readouts/timingTowerRenderer.js` may show timing-row penalty badges when the host enables those UI options, but the UI must not infer, modify, or recalculate steward decisions.
320
334
 
321
- `src/config/defaultOptions.js` owns preset resolution, telemetry module normalization, the public theme contract, and browser warmup option normalization defaults. Presets are merged before host options; theme fields are applied as package CSS variables by the all-in-one app and composable controller. This keeps sizing/color customization explicit without turning internal layout ratios into public API.
335
+ `src/config/defaultOptions.js` owns preset resolution, telemetry module normalization, browser warmup option normalization defaults, and deriving team theme selectors from normalized entry team metadata. `src/config/themeOptions.js` owns the public theme contract: semantic token resolution, generated opposite-mode value caching, known-token filtering, safe CSS color/size token validation, named derivative theme packages, component slot maps, component/team theme selectors, and legacy alias mapping for the old small color API. Presets are merged before host options; resolved theme fields are applied as package CSS variables by the all-in-one app and composable controller. Runtime selected-team theme context is cached so theme variables are not recomputed during normal render loops. This keeps sizing/color customization explicit without turning internal layout ratios into public API.
322
336
 
323
337
  `src/config/restartOptions.js` owns shared restart-option merging for both `mountF1Simulator().restart()` and `createPaddockSimulator().restart()`. It preserves omitted driver/entry data, merges nested UI banner and asset texture groups, and resets prior UI/theme state when a restart supplies a new preset so the two browser mount surfaces cannot drift.
324
338
 
@@ -326,7 +340,7 @@ Race completion is owned by `src/simulation/raceSimulation.js`. The simulation r
326
340
 
327
341
  Composable hosts may choose where each package-owned component root is placed, but they still receive package-generated markup through the public mount functions. The controller marks each mounted root as an `f1-sim-component` styling scope so standalone pieces receive the same package variables as the all-in-one shell.
328
342
 
329
- `src/styles.css` styles the generated shell, imports package fonts, caps timing-tower width for readability, and owns the fixed-height timing-list scroll behavior. Timing rows are fixed grid rows stacked from the top, so rank positions do not stretch or redistribute when the number of entries changes. The timing tower owns its runtime interval-vs-leader-gap switch, while `F1SimulatorApp` reads both seconds and whole-lap gap values from the race snapshot and formats positive lap deficits as `+N` before seconds. Fixed hidden timing-line creation, crossing timestamps, and seconds-gap calculation stay inside `src/simulation/timing/`. Sector-map surfaces read `lapTelemetry.sectorProgress` for bar fill and `lapTelemetry.liveSectors` for live active-sector time; the simulation owns those values so UI components do not infer active split state from DOM state. Future sectors are cleared by the simulation and ignored defensively by the renderer, so banner/sidebar sector nodes cannot keep stale timing text or yellow/purple/green performance classes after the active sector changes. Component templates include lightweight package-owned loading overlays; `F1SimulatorApp` removes them after startup initialization has completed.
343
+ `src/styles.css` styles the generated shell, uses package-owned system font stacks, caps timing-tower width for readability, and owns the timing-list scroll behavior. Timing rows are compact content-sized grid rows stacked from the top, with `ui.timingEntryVerticalPadding` setting one shared pixel padding value above and below every entry, so rank positions do not stretch or redistribute when the number of entries changes. The timing tower owns its runtime interval-vs-leader-gap switch, while `F1SimulatorApp` reads both seconds and whole-lap gap values from the race snapshot and formats positive lap deficits as `+N` before seconds. The compact `Int`/`Gap` header toggle is a timing-tower style contract, not host CSS: the grid reserves a stable gap column, the header labels and toggle share one vertical center, the visible pill stays inside the header, and the clickable target remains `44px` without changing tower width. Fixed hidden timing-line creation, crossing timestamps, and seconds-gap calculation stay inside `src/simulation/timing/`. Sector-map surfaces read `lapTelemetry.sectorProgress` for bar fill and `lapTelemetry.liveSectors` for live active-sector time; the simulation owns those values so UI components do not infer active split state from DOM state. Future sectors are cleared by the simulation and ignored defensively by the renderer, so banner/sidebar sector nodes cannot keep stale timing text or yellow/purple/green performance classes after the active sector changes. Component templates include lightweight package-owned loading overlays; `F1SimulatorApp` removes them after startup initialization has completed.
330
344
 
331
345
  The host page should only provide:
332
346
 
@@ -0,0 +1,72 @@
1
+ # Component Inventory
2
+
3
+ This inventory maps package-owned UI surfaces to their shared bases, owner modules, runtime wiring, and intentional differences. Keep it synchronized with `src/ui/*Template.js`, `src/app/domBindings.js`, `src/app/readouts/*`, `src/app/banners/*`, `src/app/layoutSupport.js`, `src/app/raceOverlayClearanceSupport.js`, and `src/api/PaddockSimulatorController.js`.
4
+
5
+ ## Shared Ownership Rules
6
+
7
+ - `src/ui/templateUtils.js` owns generic generated-markup helpers: escaping, loading overlays, unsupported-size overlays, and other repeated inert markup primitives.
8
+ - `src/api/PaddockSimulatorController.js` owns composable root mounting, root CSS variables, layout-support installation, and the composite query surface used by `F1SimulatorApp`.
9
+ - `src/app/domBindings.js` is the only runtime selector map for package-generated DOM. Renderers and controllers consume cached nodes from that map instead of rediscovering common readouts. When one simulator can legitimately own multiple copies of a readout surface, the binding should expose plural collections and the focused renderer should fan out updates.
10
+ - `src/app/layoutSupport.js` owns component support-envelope checks and `data-paddock-size-unsupported` state. It should stay generic and component-id driven.
11
+ - `src/app/layoutScheduler.js` owns RAF-backed layout sync coalescing and cleanup cancellation for component measurement helpers.
12
+ - `src/app/raceOverlayClearanceSupport.js` owns the measured lower-third vs embedded timing-tower clearance path. It is intentionally limited to race-canvas panels with timing towers, caches static tower/banner/toggle nodes per panel, and re-queries only dynamic timing rows during syncs.
13
+ - `src/config/themeOptions.js` owns theme token, component slot, component selector, and selected-team variable application. Templates expose `data-paddock-component`; runtime applies variables.
14
+
15
+ ## Package Components
16
+
17
+ | Component or variant | Public mount or composition | Owner module | Shared base | Runtime owner | Intentional differences |
18
+ | --- | --- | --- | --- | --- | --- |
19
+ | All-in-one shell | `mountF1Simulator()` | `src/ui/shellTemplate.js` | Composes race controls, optional external camera controls, timing tower, race canvas, telemetry stack, shell unsupported overlay | `F1SimulatorApp` | Owns `data-f1-simulator-shell`, workspace/grid classes, layout preset classes, and the package root styling scope. Not individually mounted by composable hosts. |
20
+ | Race controls | `mountRaceControls()` and all-in-one shell | `src/ui/raceControlsTemplate.js` | Component surface frame helper, `createSafetyCarControlMarkup({ compact: true })`, loading overlay, unsupported overlay | `controlStateRenderer.js` plus `F1SimulatorApp.bindControls()` | Includes title, kicker, safe backlink, safety-car button, and restart. The compact safety button has no standalone component wrapper. |
21
+ | Safety-car control | `mountSafetyCarControl()` and drawer/race-controls compact embed | `src/ui/raceControlsTemplate.js` | Component surface frame helper for standalone, same `data-safety-car` button and control-state renderer | `controlStateRenderer.js` plus `F1SimulatorApp.bindControls()` | Standalone variant gets `data-paddock-component="safety-car-control"` and unsupported-size overlay. Compact variants are plain subcontrols inside race controls or drawer toolbar. |
22
+ | Camera controls | `mountCameraControls()`, all-in-one external controls, embedded race-canvas controls, drawer toolbar | `src/ui/cameraControlsTemplate.js` | Component surface frame helper, shared `data-camera-mode`, zoom, banner mute, optional speed button, loading overlay, unsupported overlay | `controlStateRenderer.js`, `CameraController`, `F1SimulatorApp.bindControls()` | External variant adds `camera-controls--external`; embedded variant omits that class. Driver camera and simulation-speed buttons are option gated. |
23
+ | Timing tower | `mountTimingTower()`, all-in-one shell, embedded race canvas, drawer race canvas | `src/ui/timingTowerTemplate.js` | Component surface frame helper, race-control status banner, loading overlay, unsupported overlay, shared timing gap controls | `timingTowerRenderer.js`, `raceStatusRenderer.js`, `controlStateRenderer.js`, `F1SimulatorApp.observeLayoutResize()` | Embedded variants receive generated instance ids for `aria-controls`. `domBindings.js` exposes plural timing lists/header/status nodes and focused renderers fan out rows, lap counters, status banners, gap controls, and resize observation across every mounted tower owned by the same simulator. Header gap toggle may be hidden while controller APIs remain active. Width/proportions are package-owned and must not be tuned by host CSS. |
24
+ | Race-control status banner | Timing tower internal component | `src/ui/raceControlStatusBanner.js` | Timing tower status slot | `raceStatusRenderer.js` | Internal only. It has no public mount because it is a timing-tower status affordance, not a standalone component. |
25
+ | Race canvas | `mountRaceCanvas()` and shell/drawer templates | `src/ui/raceCanvasTemplate.js` | Component surface frame helper, steward message, optional embedded camera controls, optional race-data panel, optional sector banner, loading overlay, unsupported overlay | `F1SimulatorApp`, `CameraController`, Pixi renderers, `raceStatusRenderer.js`, `stewardMessageController.js`, `raceDataBannerController.js` | Base canvas can be canvas-only; options add timing tower, race-data lower-third, sector banner, FPS, physics indicator, and responsive narrow timing reveal. Race canvas is required before runtime start. |
26
+ | Embedded timing race canvas | `mountRaceCanvas(root, { includeTimingTower: true })`, drawer template | `src/ui/raceCanvasTemplate.js` | Timing tower template plus timing reveal button and narrow responsive classes | `F1SimulatorApp.syncTimingPanelDisclosureState()`, `installRaceOverlayClearanceSupport()` | Generates unique timing-panel ids. Narrow mode makes the tower a left reveal panel and measures banner clearance only when needed. |
27
+ | Steward message | Race canvas internal component | `src/ui/bannerTemplates.js` | Shared banner markup conventions | `stewardMessageController.js` | Internal top race-control message for warnings/penalties. No public mount because it depends on race-canvas layering. |
28
+ | Race-data panel | `mountRaceDataPanel()`, race canvas lower-third, drawer lower-third | `src/ui/bannerTemplates.js` | Component surface frame helper, selected driver readouts, optional race-data telemetry detail, loading overlay, unsupported overlay | `domBindings.js` grouped panel bindings, `raceDataBannerController.js`, `telemetryRenderer.js` for sector detail | Standalone variant adds `race-data-panel--standalone`. Embedded variants are clipped/layered by race canvas. Project and radio modes share the same panel; runtime swaps content/classes across every mounted panel owned by the simulator. |
29
+ | Telemetry sector banner | `mountTelemetrySectorBanner()` and optional race-canvas embed | `src/ui/bannerTemplates.js` | Component surface frame helper, sector-bar markup, loading overlay, unsupported overlay | `telemetryRenderer.js` | Independent broadcast lower-third style sector surface. It is separate from race-data telemetry detail and should only render when explicitly mounted or embedded. |
30
+ | Telemetry stack | `mountTelemetryPanel()` and drawer content | `src/ui/telemetryTemplates.js` | Component surface frame helper, telemetry core, sector progress, lap times, sector times, optional car/driver overview, unsupported overlay | `telemetryRenderer.js`, `carOverviewRenderer.js` when overview is included | The stack owns scrolling when constrained. It may include overview by default, but drawer content intentionally disables overview. |
31
+ | Telemetry core | `mountTelemetryCore()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, loading overlay, unsupported overlay | `telemetryRenderer.js` | Scalar selected-car telemetry only. It shares selected-code/name readouts with sector banner. |
32
+ | Telemetry sectors | `mountTelemetrySectors()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, sector-bar markup, loading overlay, unsupported overlay | `telemetryRenderer.js` | Sector progress bars only. It shares sector bar selectors with race-data detail and sector banner. |
33
+ | Telemetry lap times | `mountTelemetryLapTimes()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, loading overlay, unsupported overlay | `telemetryRenderer.js` | Current, last, and best lap table only. |
34
+ | Telemetry sector times | `mountTelemetrySectorTimes()` and telemetry stack | `src/ui/telemetryTemplates.js` | Telemetry module class helper, loading overlay, unsupported overlay | `telemetryRenderer.js` | Last/best sector table only. |
35
+ | Car/driver overview | `mountCarDriverOverview()` and optional telemetry stack embed | `src/ui/carOverviewTemplate.js` | Component surface frame helper, overview field slots, loading overlay, unsupported overlay | `domBindings.js` cached field bindings, `carOverviewRenderer.js` | Standalone and stack-embedded variants share identical markup. Runtime toggles vehicle vs driver fields without remounting and updates cached label/value bindings instead of querying each field during render. |
36
+ | Race telemetry drawer | `mountRaceTelemetryDrawer()` and complete preview template | `src/ui/telemetryDrawerTemplate.js` | Component surface frame helper, camera controls, compact safety-car control, race canvas with timing/data panel, telemetry stack, unsupported overlay | `F1SimulatorApp.setTelemetryDrawerOpen()`, `controlStateRenderer.js`, normal race/telemetry renderers | Higher-level mixed template. It intentionally owns toolbar placement, drawer ids, open/closed inert state, and responsive narrow drawer behavior. |
37
+ | Loading overlay | All startup-capable public surfaces except internal-only steward/status | `src/ui/templateUtils.js` | `createLoadingMarkup(label)` | `F1SimulatorApp.completeComponentLoading()` | Removed after startup. Component labels remain surface-specific for accessibility. |
38
+ | Unsupported-size overlay | Public mount roots, shell, drawer, race/data/telemetry surfaces | `src/ui/templateUtils.js` | `createUnsupportedSizeMarkup(label)` | `installLayoutSupport()` | Display is driven by measured support state. Minimum size thresholds remain centralized in `layoutSupport.js`. |
39
+
40
+ ## Local Preview Variants
41
+
42
+ | Route or section | Package surfaces exercised | Owner in preview | Intentional difference |
43
+ | --- | --- | --- | --- |
44
+ | Templates complete broadcast | `createPaddockSimulator()` plus `mountRaceTelemetryDrawer()` | `local-preview/src/main.js`, `local-preview/src/showcaseCatalog.js` | Demonstrates the highest-level mixed workbench with race, timing, lower-third, top controls, and drawer in one package-owned template. |
45
+ | Templates dashboard, timing overlay, compact race, full dashboard | `mountF1Simulator()` with presets | `local-preview/src/main.js` | Demonstrates all-in-one shell presets and preset-first option override behavior. |
46
+ | Templates banner option | `mountRaceCanvas()` plus external `mountCameraControls()` | `local-preview/src/main.js` | Demonstrates a composable race window that owns timing tower and race-data lower-third while camera controls remain separate. |
47
+ | Components embedded window | `mountRaceCanvas()` with timing tower, data panel, sector banner plus external camera controls | `local-preview/src/main.js` | Exercises optional race-canvas embeds without the drawer wrapper. |
48
+ | Components separate pieces | Every public composable mount | `local-preview/src/main.js` `COMPONENT_PIECE_MOUNTS` | Proves separate host roots still receive package CSS variables, loading state, layout support, and shared runtime updates. The preview inventory table is the single owner of the separate-piece root id to mount-helper mapping; the telemetry drawer remains separate because it is a mixed higher-level template with its own simulator. |
49
+ | Customization route | Race canvas, overview, race-data panel, controls, timing tower, telemetry core | `local-preview/src/main.js` | Exercises theme packages, selected-team component theming, component overrides, and standalone vs embedded component theming. |
50
+ | API route | Race telemetry drawer | `local-preview/src/main.js` | Exercises controller methods and snapshot readout beside the package-owned workbench. |
51
+ | Behavior route | Multiple race-canvas variants | `local-preview/src/main.js` | Exercises timing expand/scroll, embedded camera controls, sector banner, finish panel, and deterministic behavior fixtures. |
52
+ | Stewarding route | Race canvas with race-data panel and penalty UI options | `local-preview/src/main.js` | Exercises steward message, timing penalty badges, and penalty controller APIs. |
53
+ | Playable route | Race telemetry drawer in browser expert mode | `local-preview/src/main.js`, `local-preview/src/playableKeyboardController.js` | Host-owned keyboard controller drives package-owned expert runtime; package UI remains the same drawer/canvas/timing/telemetry surfaces. |
54
+ | Policy Runner route | All-in-one browser expert mount | `local-preview/src/main.js`, `local-preview/src/policyRunner/*` | Host-owned model/controller panel drives package expert APIs and model-sense visualization without changing package component markup. |
55
+
56
+ ## Duplication Map
57
+
58
+ | Pattern | Current shared owner | Remaining duplication | Rationale or next action |
59
+ | --- | --- | --- | --- |
60
+ | Component surface frames | `templateUtils.js` | The all-in-one shell and internal steward message keep bespoke wrappers | Public component surfaces, including race controls, standalone safety-car control, timing tower, race canvas, race telemetry drawer, telemetry stack, race-data panel, telemetry sector banner, detached telemetry modules, car/driver overview, and camera controls now share the frame helper. The shell is a package root with `data-f1-simulator-shell`; steward message is an internal layered race-canvas banner. |
61
+ | Loading overlays | `templateUtils.js` | Component labels differ | Intentional accessibility labels per surface. |
62
+ | Unsupported-size overlays | `templateUtils.js` plus `layoutSupport.js` | Component labels and min-size thresholds differ | Intentional because each component has a different support envelope. |
63
+ | Sector bar markup | `templateUtils.js` | Wrapper and bar classes differ between sector telemetry, sector banner, and race-data telemetry detail | Shared helper owns selectors and initial fill/time markup; call sites pass only their intentional class differences. |
64
+ | Component styling scope | `PaddockSimulatorController.mountComponent()` and shell root class | All-in-one shell sets root class in template, composable controller sets it on host roots | Intentional because all-in-one owns the root element, while composable hosts provide roots. |
65
+ | DOM selector lookup | `domBindings.js` | `raceOverlayClearanceSupport.js` and `layoutSupport.js` perform local measurement queries | Intentional: measurement helpers need scoped live layout queries and should not enlarge app readout bindings. Timing tower state uses plural cached bindings because duplicate towers are package-owned runtime readouts. `resolveNodes()` centralizes plural collection plus singular fallback handling so renderers and resize observers do not duplicate that control flow. Overlay clearance caches static panel nodes at install time; only timing-row queries remain live because timing rows are renderer-owned dynamic markup. |
66
+ | Text and CSS custom-property writes | `domBindings.js` | Direct property assignment remains for non-custom style properties such as `style.color` | `setText()` / `setTextAll()` and `setStyleProperty()` skip unchanged writes in frequent render paths. Readout and banner renderers should use these helpers for package text and CSS custom properties instead of local write guards. |
67
+ | Car/driver overview field rendering | `domBindings.js` and `carOverviewRenderer.js` | Direct renderer fallback still supports narrow unit stubs without full DOM binding | Intentional compatibility fallback. Runtime mounts use cached field label/value bindings from `domBindings.js`, so changed overview renders do not rediscover static field children. |
68
+ | Control active state | `controlStateRenderer.js` | Event binding remains in `F1SimulatorApp` | Intentional separation: app owns events/state mutation; renderer owns button state. |
69
+ | Race-data lower-third and standalone panel | `bannerTemplates.js`, `domBindings.js`, and `raceDataBannerController.js` | Standalone adds one class and canvas embeds rely on placement | Intentional public placement difference with shared markup. Runtime state, open/dismiss controls, project mode, radio mode, and hide behavior fan out through grouped panel bindings so embedded and standalone panels do not drift. |
70
+ | Timing tower standalone vs embedded | `timingTowerTemplate.js`, `raceCanvasTemplate.js`, `timingTowerRenderer.js`, `raceStatusRenderer.js` | Embedded adds reveal button/id and narrow classes in race canvas | Intentional because disclosure state belongs to the parent race canvas, not the tower. Runtime rows, lap counters, race-control status, and gap labels are shared across standalone and embedded towers through plural DOM bindings and focused readout renderers. |
71
+ | Drawer and race-canvas responsive logic | `telemetryDrawerTemplate.js`, `raceCanvasTemplate.js`, `layoutSupport.js`, `raceOverlayClearanceSupport.js`, `layoutScheduler.js` | Drawer toolbar and race-canvas narrow reveal have distinct CSS classes | Intentional because they solve different container constraints while sharing component children. RAF scheduling/cancellation for layout measurement is shared so support checks and overlay clearance do not drift. |
72
+ | Local preview route setup | `local-preview/src/main.js` | Route-specific controllers, timers, readouts, and the components-page `COMPONENT_PIECE_MOUNTS` table remain local | Intentional host-owned demo behavior. Package component logic must not move into preview. The components-page table centralizes preview wiring of public mount helpers without changing package contracts. |
@@ -0,0 +1,77 @@
1
+ # Composable Layouts
2
+
3
+ Use composable layouts when a host page wants to place package-owned simulator pieces in separate DOM roots. The host owns placement and surrounding page layout; PaddockJS still owns generated markup, CSS variables, race state, timing state, and component behavior.
4
+
5
+ ## Create One Controller
6
+
7
+ ```js
8
+ import { createPaddockSimulator } from '@inventure71/paddockjs';
9
+
10
+ const simulator = createPaddockSimulator({
11
+ drivers,
12
+ entries,
13
+ onDriverOpen(driver) {
14
+ if (driver.link) window.location.href = driver.link;
15
+ },
16
+ });
17
+ ```
18
+
19
+ Keep one controller per race runtime. Mount the surfaces you need, then start it:
20
+
21
+ ```js
22
+ simulator.mountRaceControls(document.getElementById('sim-controls'));
23
+ simulator.mountCameraControls(document.getElementById('sim-camera-controls'));
24
+ simulator.mountSafetyCarControl(document.getElementById('sim-safety-car'));
25
+ simulator.mountRaceCanvas(document.getElementById('sim-race'), {
26
+ includeTimingTower: true,
27
+ includeRaceDataPanel: true,
28
+ timingTowerVerticalFit: 'scroll',
29
+ });
30
+ simulator.mountRaceDataPanel(document.getElementById('sim-race-data'));
31
+
32
+ await simulator.start();
33
+ ```
34
+
35
+ Controller methods are the canonical composable style. The standalone mount helper exports exist for compatibility, but new host code should prefer `simulator.mountRaceCanvas(root)` and the matching controller methods.
36
+
37
+ ## Available Surfaces
38
+
39
+ - `mountRaceControls(root)`
40
+ - `mountCameraControls(root)`
41
+ - `mountSafetyCarControl(root)`
42
+ - `mountTimingTower(root)`
43
+ - `mountRaceCanvas(root, options)`
44
+ - `mountTelemetryPanel(root)`
45
+ - `mountTelemetryCore(root)`
46
+ - `mountTelemetrySectors(root)`
47
+ - `mountTelemetrySectorBanner(root)`
48
+ - `mountTelemetryLapTimes(root)`
49
+ - `mountTelemetrySectorTimes(root)`
50
+ - `mountRaceTelemetryDrawer(root, options)`
51
+ - `mountCarDriverOverview(root)`
52
+ - `mountRaceDataPanel(root)`
53
+
54
+ `mountRaceCanvas()` can embed the timing tower and race-data lower-third when the host wants a single package-owned race window. `mountRaceTelemetryDrawer()` mounts a larger workbench with top controls, race canvas, timing tower, safety-car control, and telemetry drawer.
55
+
56
+ ## Lifecycle
57
+
58
+ ```js
59
+ await simulator.start();
60
+
61
+ simulator.selectDriver('budget');
62
+ simulator.setTimingGapMode('leader');
63
+ simulator.setPitLaneOpen(false);
64
+
65
+ simulator.restart({ trackSeed: 5051 });
66
+ simulator.destroy();
67
+ ```
68
+
69
+ Use `restart(nextOptions)` for race/data/seed changes. Use `setTheme()`, `setThemeMode()`, or `syncThemeFrom()` for presentation changes so the race does not reset.
70
+
71
+ Browser expert mode changes are not a casual restart setting. Expert mode changes ticker ownership and freezes the visual simulator until host code steps it, so only enable it intentionally for policy playback or debugging.
72
+
73
+ ## Sizing
74
+
75
+ Each mounted root receives PaddockJS component scope classes and package CSS variables. Hosts may size the outer roots, but timing-board width, broadcast proportions, camera safe area, drawer behavior, lower-thirds, focus styles, and touch targets remain package-owned.
76
+
77
+ Use `includeTimingTower: true` when the race canvas should own the timing tower placement. Use `timingTowerVerticalFit: 'scroll'` to keep a fixed race height with a scrolling timing list, or `'expand-race-view'` when the race window may grow to fit the tower.
package/docs/concepts.md CHANGED
@@ -81,13 +81,13 @@ Entry responsibilities:
81
81
  - Timing name.
82
82
  - Driver ratings.
83
83
  - Vehicle ratings.
84
- - Optional team object with `id`, `name`, `color`, and `icon`.
84
+ - Optional team object with `id`, `name`, `color`, `icon`, and `theme`.
85
85
  - Driver and vehicle rating components for the car/driver overview panel.
86
86
  - Optional driver and vehicle `customFields` for extra overview metadata.
87
87
 
88
88
  ## Team
89
89
 
90
- A team is entry-level metadata used for race identity and current pit-lane behavior. The timing tower uses the team icon in its team column. Team color defaults to the car color when omitted. When pit stops are enabled, team color marks the shared service area, queue point, and garage boxes, and optional `team.pitCrew` stats can affect service-time variability.
90
+ A team is entry-level metadata used for race identity, theme selection, and current pit-lane behavior. The timing tower uses the team icon in its team column. Team color defaults to the car color when omitted. `team.theme` can point to a named package theme for team-aware component selectors. When pit stops are enabled, team color marks the shared service area, queue point, and garage boxes, and optional `team.pitCrew` stats can affect service-time variability.
91
91
 
92
92
  ## Driver Ratings
93
93
 
@@ -123,6 +123,8 @@ This gives each observation a `Float32Array` vector and avoids per-frame object/
123
123
 
124
124
  For visual debugging, use `output: 'full'` or `output: 'object'` so you can display human-readable senses. Policy Runner uses the active observation values; it should not recompute sharper or different senses than the model receives.
125
125
 
126
+ For HTTP policy-server integrations, keep the transport compact. Policy Runner sends `protocolVersion: 2`; reset/init receives `actionSpec`, `observationSpec`, configuration, `previousActionFields`, and `metricFields`, while each `/policy/decide-batch` request sends `driverIds` plus compact `vectors`, `previousActions`, `metrics`, and `events` arrays aligned by driver index. Do not expect full observation objects, schemas, full snapshots, or track metadata in the per-decision payload. In this compact mode the Policy Runner senses panel shows the active vector shape instead of object/ray diagnostics.
127
+
126
128
  ## Headless Runtime
127
129
 
128
130
  Use the browser-free environment when training or evaluating outside a mounted page:
@@ -137,7 +139,7 @@ const env = createPaddockEnvironment({
137
139
  drivers,
138
140
  entries,
139
141
  controlledDrivers: ['agent-1', 'agent-2'],
140
- physicsMode: 'simulator',
142
+ physicsMode: 'advanced',
141
143
  participantInteractions: { defaultProfile: 'batch-training' },
142
144
  observation: {
143
145
  profile: 'physical-driver',
@@ -169,15 +171,13 @@ while (!result.done) {
169
171
  Use browser expert mode when you want to watch the model drive:
170
172
 
171
173
  ```js
172
- import {
173
- createPaddockDriverControllerLoop,
174
- mountF1Simulator,
175
- } from '@inventure71/paddockjs';
174
+ import { createPaddockDriverControllerLoop } from '@inventure71/paddockjs/environment';
175
+ import { mountF1Simulator } from '@inventure71/paddockjs';
176
176
 
177
177
  const simulator = await mountF1Simulator(root, {
178
178
  drivers,
179
179
  entries,
180
- physicsMode: 'simulator',
180
+ physicsMode: 'advanced',
181
181
  expert: {
182
182
  enabled: true,
183
183
  controlledDrivers: ['agent-1'],