@inventure71/paddockjs 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 846f388: Add detached telemetry surfaces, sector timing status colors, a broadcast sector banner, and a race telemetry drawer template with embedded timing tower, safety-car control, and lower-third race data.
8
+
9
+ ### Patch Changes
10
+
11
+ - 21cf4e5: Document the official release workflow and switch automated publishing to npm trusted publishing through GitHub Actions.
12
+
3
13
  All notable changes to this project will be documented in this file.
4
14
 
5
15
  The npm release workflow updates this changelog from committed Changesets.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # PaddockJS
2
2
 
3
- This folder is the extractable F1 simulator component. It owns the simulator source, bundled simulator assets, CSS, demo data, and the public mount API.
3
+ This folder is the extractable F1 simulator component. It owns the simulator source, bundled simulator assets, CSS, demo data, and the public mount API.
4
4
 
5
5
  ## Install
6
6
 
@@ -28,6 +28,7 @@ The repo now includes the full package-release boundary:
28
28
  - public TypeScript declarations in `src/index.d.ts`
29
29
  - GitHub Actions CI in `.github/workflows/ci.yml`
30
30
  - Changesets release automation in `.github/workflows/release.yml`
31
+ - npm trusted publishing support through GitHub Actions OIDC
31
32
 
32
33
  Useful commands:
33
34
 
@@ -95,8 +96,13 @@ simulator.mountSafetyCarControl(document.getElementById('sim-safety-car'));
95
96
  simulator.mountTimingTower(document.getElementById('sim-timing'));
96
97
  simulator.mountRaceCanvas(document.getElementById('sim-race'), {
97
98
  includeRaceDataPanel: true,
99
+ includeTelemetrySectorBanner: true,
98
100
  });
99
- simulator.mountTelemetryPanel(document.getElementById('sim-telemetry'));
101
+ simulator.mountTelemetryCore(document.getElementById('sim-telemetry-core'));
102
+ simulator.mountTelemetrySectors(document.getElementById('sim-telemetry-sectors'));
103
+ simulator.mountTelemetrySectorBanner(document.getElementById('sim-telemetry-sector-banner'));
104
+ simulator.mountTelemetryLapTimes(document.getElementById('sim-telemetry-laps'));
105
+ simulator.mountTelemetrySectorTimes(document.getElementById('sim-telemetry-sector-times'));
100
106
  simulator.mountCarDriverOverview(document.getElementById('sim-overview'));
101
107
 
102
108
  await simulator.start();
@@ -108,10 +114,19 @@ The race canvas can also own the timing tower when a host wants a single reusabl
108
114
  simulator.mountRaceCanvas(document.getElementById('sim-race'), {
109
115
  includeTimingTower: true,
110
116
  includeRaceDataPanel: true,
117
+ includeTelemetrySectorBanner: true,
111
118
  timingTowerVerticalFit: 'scroll',
112
119
  });
113
120
  ```
114
121
 
122
+ For a packaged race-window template with a right-side telemetry drawer:
123
+
124
+ ```js
125
+ simulator.mountRaceTelemetryDrawer(document.getElementById('sim-race-workbench'), {
126
+ timingTowerVerticalFit: 'expand-race-view',
127
+ });
128
+ ```
129
+
115
130
  `drivers` is the host-owned project/pilot list. `entries` is the optional driver/car/team pairing sheet. The car/driver overview uses the existing driver and vehicle rating components from each entry. Driver and vehicle entries can also include `customFields` as extra label/value metadata. Entries can include `team: { id, name, color, icon }`; the timing tower uses the team icon and defaults team color to the car color when omitted. Assets, including the default car image and generic driver helmet, are bundled by default, so the host website does not need to provide simulator images or textures.
116
131
 
117
132
  The returned object supports:
@@ -139,6 +154,7 @@ ui: {
139
154
  cameraControls: 'external',
140
155
  showFps: false,
141
156
  telemetryIncludesOverview: false,
157
+ telemetryModules: ['core', 'sectors', 'lapTimes', 'sectorTimes'],
142
158
  raceDataBanners: {
143
159
  initial: 'project',
144
160
  enabled: ['project', 'radio'],
@@ -152,13 +168,13 @@ ui: {
152
168
 
153
169
  If `trackSeed` is omitted, each mounted browser simulator creates a fresh procedural circuit. Passing `trackSeed` makes the track deterministic so multiple embeds can share the same generated circuit; repeated procedural seeds are cached within the page runtime.
154
170
 
155
- `layoutPreset: 'left-tower-overlay'` creates a left broadcast gutter inside the race view, frames the camera around the remaining race area, and places the timing tower in that gutter without covering camera controls. The project/radio lower-third stays inside the race window and can intentionally render over the timing sidebar instead of shrinking around it. Composable hosts should pass `{ includeRaceDataPanel: true }` to `mountRaceCanvas()` when they want that lower-third clipped and layered by the race window; `mountRaceDataPanel()` remains available for hosts that intentionally want the banner as a standalone surface. `includeTimingTower: true` embeds the timing tower directly inside the race canvas and reserves camera space from the measured tower gutter. `raceDataBanners.initial` selects the starting banner state (`'project'`, `'radio'`, or `'hidden'`), while `raceDataBanners.enabled` chooses which banner types may appear. `raceDataBannerSize: 'auto'` uses the race space to the right of the timing board when it is wide enough and falls back to the full lower-third overlap when it is not; `'custom'` preserves the default CSS-variable-driven lower-third size for hosts that want to tune their own banner geometry. `timingTowerVerticalFit: 'expand-race-view'` lets the race window grow tall enough for the tower; `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same fit values can be passed to `mountRaceCanvas()` when `includeTimingTower` is enabled. Standalone timing towers are capped by `--timing-board-max-width` and fill their mount root height; placing the root in a fixed-height container makes only the timing entries scroll. Timing entries always stack from the top as fixed rows, so P1/P2 occupy the same vertical positions whether the race has 2 cars or 20. The timing tower includes an `Int`/`Gap` broadcast switch: `Int` shows the interval to the car ahead, while `Gap` shows total gap to the leader. `cameraControls: 'external'` moves view controls out of the canvas so hosts can mount them with `mountCameraControls()`. `showFps: false` hides the FPS readout. `telemetryIncludesOverview: false` keeps telemetry text-only so hosts can mount `mountCarDriverOverview()` separately.
171
+ `layoutPreset: 'left-tower-overlay'` creates a left broadcast gutter inside the race view, frames the camera around the remaining race area, and places the timing tower in that gutter without covering camera controls. The project/radio lower-third stays inside the race window and can intentionally render over the timing sidebar instead of shrinking around it. Composable hosts should pass `{ includeRaceDataPanel: true }` to `mountRaceCanvas()` when they want that lower-third clipped and layered by the race window; `mountRaceDataPanel()` remains available for hosts that intentionally want the banner as a standalone surface. `includeTimingTower: true` embeds the timing tower directly inside the race canvas and reserves camera space from the measured tower gutter when the tower is a side overlay. In narrow mobile hosts, package CSS stacks the embedded timing tower full-width and the camera stops reserving a fake left gutter. `mountRaceTelemetryDrawer()` is a higher-level template that mounts the race canvas, embedded timing tower, safety-car control, lower-third, and a right telemetry drawer together; opening the drawer reduces the race area instead of overlaying it, and closing it removes the drawer from interaction. `raceDataBanners.initial` selects the starting banner state (`'project'`, `'radio'`, or `'hidden'`), while `raceDataBanners.enabled` chooses which banner types may appear. `raceDataBannerSize: 'auto'` uses the race space to the right of the timing board when it is wide enough and falls back to the full lower-third overlap when it is not; `'custom'` preserves the default CSS-variable-driven lower-third size for hosts that want to tune their own banner geometry. `timingTowerVerticalFit: 'expand-race-view'` lets the race window grow tall enough for the tower; `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same fit values can be passed to `mountRaceCanvas()` when `includeTimingTower` is enabled. Standalone timing towers are capped by `--timing-board-max-width` and fill their mount root height; placing the root in a fixed-height container makes only the timing entries scroll. Timing entries always stack from the top as fixed rows, so P1/P2 occupy the same vertical positions whether the race has 2 cars or 20. The timing tower includes an `Int`/`Gap` broadcast switch: `Int` shows the interval to the car ahead, while `Gap` shows total gap to the leader. `cameraControls: 'external'` moves view controls out of the canvas so hosts can mount them with `mountCameraControls()`. `showFps: false` hides the FPS readout. `telemetryIncludesOverview: false` keeps the car/driver overview out of the telemetry stack so hosts can mount `mountCarDriverOverview()` separately. `telemetryModules` controls optional package-owned telemetry surfaces: `core`, `sectors`, `lapTimes`, and `sectorTimes`; those same surfaces are also available as fully detached mount methods.
156
172
 
157
173
  Mounted package surfaces include a lightweight red start-light loading overlay. The runtime removes each overlay after PixiJS, assets, controls, and the initial readouts are ready.
158
174
 
159
175
  The runtime also pauses its render ticker when the race canvas is offscreen or the browser tab is hidden. This keeps pages with multiple PaddockJS embeds responsive without requiring host code to manually start and stop each simulator.
160
176
 
161
- Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; callback errors are routed to `onError` when provided and do not stop the simulator loop. Race snapshots include per-car interval timing, leader-gap timing, calibrated `speedKph`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field finishes. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
177
+ Lifecycle callbacks are optional and host-owned. PaddockJS emits `onLoadingChange`, `onReady`, `onError`, `onDriverSelect`, `onRaceEvent`, `onLapChange`, and `onRaceFinish`; callback errors are routed to `onError` when provided and do not stop the simulator loop. Race snapshots include per-car interval timing, leader-gap timing, calibrated `speedKph`, automatic `track.sectors`, per-car `lapTelemetry`, finish state, `raceControl.winner` after the first finisher, and final `raceControl.classification` only after the whole field finishes. The field then circulates in safety-car mode and the race canvas shows a package-owned winner banner.
162
178
 
163
179
  ## License
164
180
 
@@ -48,7 +48,12 @@ host page
48
48
  -> simulator.mountSafetyCarControl(safetyCarRoot)
49
49
  -> simulator.mountTimingTower(timingRoot)
50
50
  -> simulator.mountRaceCanvas(canvasRoot, { includeRaceDataPanel })
51
- -> simulator.mountTelemetryPanel(telemetryRoot)
51
+ -> simulator.mountTelemetryCore(coreTelemetryRoot)
52
+ -> simulator.mountTelemetrySectors(sectorGraphRoot)
53
+ -> simulator.mountTelemetrySectorBanner(sectorBannerRoot)
54
+ -> simulator.mountTelemetryLapTimes(lapTimesRoot)
55
+ -> simulator.mountTelemetrySectorTimes(sectorTimesRoot)
56
+ -> simulator.mountRaceTelemetryDrawer(raceWorkbenchRoot) [optional template alternative]
52
57
  -> simulator.mountRaceDataPanel(raceDataRoot) [optional standalone alternative]
53
58
  -> simulator.start()
54
59
  -> new F1SimulatorApp(compositeRoot, resolvedOptions)
@@ -118,6 +123,7 @@ Responsibilities:
118
123
  - Car creation.
119
124
  - Race ordering.
120
125
  - Lap calculation.
126
+ - Per-car lap and sector telemetry.
121
127
  - Timing history.
122
128
  - DRS eligibility.
123
129
  - Collision response.
@@ -127,13 +133,13 @@ Responsibilities:
127
133
 
128
134
  `src/simulation/vehiclePhysics.js` owns vehicle integration and surface physics.
129
135
 
130
- `src/simulation/trackModel.js` owns track construction, procedural track generation, DRS zones, and nearest-track queries. Browser mounts generate a fresh procedural seed when `trackSeed` is omitted; explicit procedural track seeds are cached so repeated mounts with the same seed do not regenerate or rebuild the same model.
136
+ `src/simulation/trackModel.js` owns track construction, procedural track generation, automatic three-sector metadata, DRS zones, and nearest-track queries. Browser mounts generate a fresh procedural seed when `trackSeed` is omitted; explicit procedural track seeds are cached so repeated mounts with the same seed do not regenerate or rebuild the same model.
131
137
 
132
138
  `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`.
133
139
 
134
140
  `src/rendering/renderSnapshot.js` owns interpolation for rendering.
135
141
 
136
- 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. Long project-radio delays are also treated as stale schedule state instead of replaying every missed lower-third transition.
142
+ 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. Runtime readouts avoid redundant DOM writes for unchanged text, timing markup, finish classification, and static selected-car overview data. Long project-radio delays are also treated as stale schedule state instead of replaying every missed lower-third transition.
137
143
 
138
144
  ## Data Layer
139
145
 
@@ -157,12 +163,15 @@ The app runtime pauses its PixiJS ticker when the race canvas is outside the vie
157
163
  - Timing tower.
158
164
  - Race canvas.
159
165
  - Race-data panel.
160
- - Telemetry panel.
166
+ - Telemetry stack template.
167
+ - Detached telemetry core, sector graph, lap-time table, and sector-time table.
168
+ - Broadcast sector banner.
169
+ - Race telemetry drawer template.
161
170
  - Car/driver overview panel.
162
171
 
163
- `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`. The telemetry panel can embed the car/driver overview, but the overview is also a separately mountable package-owned component. 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. `F1SimulatorApp` 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. Hosts should not provide the internal simulator markup or tune preset internals with raw sizing options.
172
+ `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; the app renders them from `car.lapTelemetry` without requiring host-owned DOM. The telemetry stack can embed the car/driver overview, but the overview is also a separately mountable package-owned component. The broadcast sector banner is a separate lower-third-style sector graph that can be mounted standalone or embedded inside the race canvas; `F1SimulatorApp` binds the selected car name/code/color to it through the same readout path as the other telemetry surfaces. The race telemetry drawer template composes a race canvas with embedded timing tower, safety-car control, lower-third, sector banner, and detached telemetry components in a right drawer; the drawer's open/close state and safety-car button are owned by `F1SimulatorApp`. 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 inherits the 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. `F1SimulatorApp` 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. 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.
164
173
 
165
- `src/config/defaultOptions.js` owns preset resolution and the public theme contract. 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.
174
+ `src/config/defaultOptions.js` owns preset resolution, telemetry module normalization, and the public theme contract. 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.
166
175
 
167
176
  Race completion is owned by `src/simulation/raceSimulation.js`. The app layer reads `raceControl.finished`, `winner`, and `classification` from snapshots, renders the winner banner, and emits lifecycle callbacks. UI code must not infer finish state from lap text or timing rows.
168
177
 
package/docs/concepts.md CHANGED
@@ -131,6 +131,10 @@ Progress is the wrapped distance around the current lap.
131
131
 
132
132
  Race distance is cumulative and can increase beyond one lap. Ranking uses race distance, not wrapped progress.
133
133
 
134
+ ## Sectors
135
+
136
+ Every built track is divided into three equal sectors, published as `track.sectors`. Sector timing is derived from cumulative race distance crossing those sector boundaries, not from UI state.
137
+
134
138
  ## Snapshot
135
139
 
136
140
  A snapshot is the read-only state returned by the race simulation and exposed through `getSnapshot()`.
@@ -146,7 +150,7 @@ Snapshots include:
146
150
  - Events from the last step.
147
151
  - Ordered cars with telemetry and setup data.
148
152
 
149
- Per-car timing exposes both interval to the car ahead and cumulative gap to the leader. Per-car speed and distance display fields are calibrated through the simulator unit conversion helpers instead of treating rendered world units as meters.
153
+ Per-car timing exposes both interval to the car ahead and cumulative gap to the leader. Per-car `lapTelemetry` exposes current lap, current sector, current/last/best lap times, current/last/best sector times, sector progress, and sector performance status. Sector performance status marks completed sector times as `overall-best`, `personal-best`, or `slower`, which drives the purple/green/yellow timing colors in sector graphs and tables. Per-car speed and distance display fields are calibrated through the simulator unit conversion helpers instead of treating rendered world units as meters.
150
154
 
151
155
  ## Render Snapshot
152
156
 
@@ -54,6 +54,7 @@ const simulator = createPaddockSimulator({
54
54
  simulator.mountRaceCanvas(canvasRoot, {
55
55
  includeRaceDataPanel: true,
56
56
  includeTimingTower: true,
57
+ includeTelemetrySectorBanner: true,
57
58
  timingTowerVerticalFit: 'scroll',
58
59
  });
59
60
  await simulator.start();
@@ -67,10 +68,45 @@ simulator.mountCameraControls(cameraControlsRoot);
67
68
  simulator.mountSafetyCarControl(safetyCarRoot);
68
69
  simulator.mountTimingTower(timingRoot);
69
70
  simulator.mountTelemetryPanel(telemetryRoot);
71
+ simulator.mountTelemetryCore(coreTelemetryRoot);
72
+ simulator.mountTelemetrySectors(sectorGraphRoot);
73
+ simulator.mountTelemetrySectorBanner(sectorBannerRoot);
74
+ simulator.mountTelemetryLapTimes(lapTimesRoot);
75
+ simulator.mountTelemetrySectorTimes(sectorTimesRoot);
76
+ simulator.mountRaceTelemetryDrawer(raceWorkbenchRoot, {
77
+ timingTowerVerticalFit: 'expand-race-view',
78
+ });
70
79
  simulator.mountCarDriverOverview(overviewRoot);
71
80
  simulator.mountRaceDataPanel(raceDataRoot);
72
81
  ```
73
82
 
83
+ ## Lap Telemetry Snapshot
84
+
85
+ Each `car.lapTelemetry` snapshot includes current/last/best lap and sector timing, plus sector performance classes used by package UI:
86
+
87
+ ```js
88
+ {
89
+ currentLap,
90
+ currentSector,
91
+ currentLapTime,
92
+ currentSectorElapsed,
93
+ currentSectorProgress,
94
+ currentSectors,
95
+ lastLapTime,
96
+ bestLapTime,
97
+ lastSectors,
98
+ bestSectors,
99
+ completedLaps,
100
+ sectorPerformance: {
101
+ current: ['overall-best', 'personal-best', null],
102
+ last: [null, 'slower', null],
103
+ best: ['overall-best', null, 'personal-best'],
104
+ },
105
+ }
106
+ ```
107
+
108
+ `overall-best` means fastest sector time currently known across the field, `personal-best` means that driver's own fastest non-overall sector, and `slower` marks a completed sector that is slower than the driver's personal best. Missing or in-progress sector values are `null`.
109
+
74
110
  ## Required Options
75
111
 
76
112
  `drivers` is required and must be a non-empty array.
@@ -253,6 +289,13 @@ ui: {
253
289
  showFps: true,
254
290
  showTimingTower: true,
255
291
  showTelemetry: true,
292
+ telemetryIncludesOverview: true,
293
+ telemetryModules: {
294
+ core: true,
295
+ sectors: true,
296
+ lapTimes: true,
297
+ sectorTimes: true,
298
+ },
256
299
  showRaceDataPanel: true,
257
300
  raceDataBanners: {
258
301
  initial: 'project',
@@ -268,12 +311,14 @@ ui: {
268
311
  - `showFps`: controls whether the race canvas renders the FPS readout.
269
312
  - `showRaceDataPanel`: controls whether the precombined shell includes the project/radio lower-third inside the race window.
270
313
  - `showTimingTower`, `showTelemetry`: reserved component visibility flags for host layout decisions.
314
+ - `telemetryIncludesOverview`: controls whether the telemetry stack template embeds the car/driver overview. Composable hosts can also pass `mountTelemetryPanel(root, { includeOverview: false })`.
315
+ - `telemetryModules`: controls optional telemetry surfaces inside stack/drawer templates. The default object enables `core` scalar readouts, `sectors` progress bars, `lapTimes`, and `sectorTimes`. It can also be `false` to disable all telemetry modules, or an array such as `['sectors', 'lapTimes']` to render only named modules. These modules are also individually mountable with `mountTelemetryCore`, `mountTelemetrySectors`, `mountTelemetryLapTimes`, and `mountTelemetrySectorTimes`.
271
316
  - `raceDataBanners.initial`: `'project'`, `'radio'`, or `'hidden'`. This controls which lower-third appears first in the precombined shell.
272
317
  - `raceDataBanners.enabled`: array containing `'project'` and/or `'radio'`. Disabled banner types never appear, including after driver selection.
273
318
  - `raceDataBannerSize`: `'custom'` preserves the default lower-third geometry and exposes package CSS variables for host tuning. `'auto'` uses the race space to the right of the timing board when there is enough room and falls back to full lower-third overlap when there is not.
274
319
  - `timingTowerVerticalFit`: `'expand-race-view'` lets the combined race window grow to contain the timing tower. `'scroll'` keeps the race window height and scrolls the timing list inside the cropped tower. The same values can be passed to `mountRaceCanvas(root, { includeTimingTower: true, timingTowerVerticalFit })` for an embedded composable timing tower.
275
320
 
276
- No UI option exists for raw timing-tower width, max width, or horizontal ratio. The timing tower is capped by the package CSS variable `--timing-board-max-width` because very wide timing boards read poorly. Host pages can scale the whole simulator by changing the mount container, but package-owned layout presets keep their internal proportions inside PaddockJS. For standalone timing towers, give the mount root a fixed height when a fixed vertical footprint is needed; the package keeps the frame inside that height and scrolls only the timing entries.
321
+ No UI option exists for raw timing-tower width, max width, or horizontal ratio. The timing tower is capped by the package CSS variable `--timing-board-max-width` because very wide timing boards read poorly. Host pages can scale the whole simulator by changing the mount container, but package-owned layout presets keep their internal proportions inside PaddockJS. For standalone timing towers, give the mount root a fixed height when a fixed vertical footprint is needed; the package keeps the frame inside that height and scrolls only the timing entries. Narrow hosts are handled internally: side-gutter timing towers become stacked/full-width, embedded timing towers stop behaving like desktop side overlays, and the camera safe area stops reserving a left gutter when the measured timing board is effectively full-width.
277
322
 
278
323
  ## Theme And Sizing Contract
279
324
 
@@ -315,7 +360,39 @@ After every car completes `totalLaps`, `getSnapshot()` returns:
315
360
  }
316
361
  ```
317
362
 
318
- Cars also include `team`, `speedKph`, `distanceMeters`, `gapAheadMeters`, `gapAheadSeconds`, `intervalAheadSeconds`, `leaderGapSeconds`, `finished`, `finishTime`, and `classifiedRank`. `gapAheadSeconds` and `intervalAheadSeconds` are the interval to the car directly ahead. `leaderGapSeconds` is the cumulative gap to P1. The first car to finish sets `raceControl.winner` and receives a `car-finish` event, but the race keeps running until all cars finish. After final classification, the field continues under safety-car behavior.
363
+ Cars also include `team`, `speedKph`, `distanceMeters`, `gapAheadMeters`, `gapAheadSeconds`, `intervalAheadSeconds`, `leaderGapSeconds`, `finished`, `finishTime`, `classifiedRank`, and `lapTelemetry`. `gapAheadSeconds` and `intervalAheadSeconds` are the interval to the car directly ahead. `leaderGapSeconds` is the cumulative gap to P1. The first car to finish sets `raceControl.winner` and receives a `car-finish` event, but the race keeps running until all cars finish. After final classification, the field continues under safety-car behavior.
364
+
365
+ ## Track And Lap Telemetry Snapshot
366
+
367
+ Every built track is automatically divided into three equal sectors. `snapshot.track.sectors` exposes:
368
+
369
+ ```js
370
+ [
371
+ { index: 1, id: 's1', label: 'S1', start, end, startRatio, endRatio, length },
372
+ { index: 2, id: 's2', label: 'S2', start, end, startRatio, endRatio, length },
373
+ { index: 3, id: 's3', label: 'S3', start, end, startRatio, endRatio, length },
374
+ ]
375
+ ```
376
+
377
+ Each car exposes `lapTelemetry`:
378
+
379
+ ```js
380
+ {
381
+ currentLap,
382
+ currentSector,
383
+ currentLapTime,
384
+ currentSectorElapsed,
385
+ currentSectorProgress,
386
+ currentSectors: [s1Time, s2Time, s3Time],
387
+ lastLapTime,
388
+ bestLapTime,
389
+ lastSectors: [s1Time, s2Time, s3Time],
390
+ bestSectors: [s1Time, s2Time, s3Time],
391
+ completedLaps,
392
+ }
393
+ ```
394
+
395
+ Times are seconds or `null` when no timing exists yet. `currentSectorProgress` is `0..1` within the active sector.
319
396
 
320
397
  ## Unit Conversion
321
398
 
@@ -352,7 +429,12 @@ Composable controllers additionally expose:
352
429
  - `mountSafetyCarControl(root)`: renders a package-owned safety-car button that binds to the same race-control state as other safety buttons.
353
430
  - `mountTimingTower(root)`: renders the timing tower component.
354
431
  - `mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, timingTowerVerticalFit })`: renders the PixiJS canvas host, optional FPS, start lights, and optionally embedded camera controls. Pass `includeRaceDataPanel: true` to place the project/radio lower-third inside the race window so it shares race-canvas clipping and layering. Pass `includeTimingTower: true` to place the timing tower inside the race canvas; `timingTowerVerticalFit: 'expand-race-view'` grows the canvas to the tower height, while `'scroll'` keeps the canvas height and scrolls timing rows inside the tower frame. This is required before `start()`.
355
- - `mountTelemetryPanel(root, { includeOverview })`: renders selected-car text telemetry. It includes the car/driver overview by default unless `includeOverview: false` is passed or `ui.telemetryIncludesOverview` is `false`.
432
+ - `mountTelemetryPanel(root, { includeOverview })`: renders the package-owned telemetry stack template. The stack is only a composition of detached telemetry surfaces; it includes the car/driver overview by default unless `includeOverview: false` is passed or `ui.telemetryIncludesOverview` is `false`.
433
+ - `mountTelemetryCore(root)`: renders selected-car scalar telemetry only.
434
+ - `mountTelemetrySectors(root)`: renders the live sector progress graph only.
435
+ - `mountTelemetryLapTimes(root)`: renders current, last, and best lap timing only.
436
+ - `mountTelemetrySectorTimes(root)`: renders last and best sector timing only.
437
+ - `mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen })`: renders a template that combines race canvas, embedded timing tower, lower-third banner, and a right-side telemetry drawer. The drawer uses the detached telemetry components and takes layout space from the race window when opened.
356
438
  - `mountCarDriverOverview(root)`: renders the package-owned car/driver overview as a separate component with a Car/Driver toggle, center visual, and linked stat cells from the existing driver/vehicle rating components.
357
439
  - `mountRaceDataPanel(root)`: renders the project/race-data lower-third as a separate component for hosts that intentionally want it outside the race canvas.
358
440
  - `start()`: initializes PixiJS, binds mounted controls, and starts the simulation loop.
package/docs/rules.md CHANGED
@@ -59,6 +59,8 @@ The race engine uses simulator units internally. `src/simulation/units.js` conve
59
59
 
60
60
  Lap is computed from each car's cumulative race distance over the track length. Total laps are provided by mount options and default to `10`.
61
61
 
62
+ Each track is automatically divided into three equal sectors. Sector and lap telemetry is recorded when a car's cumulative race distance crosses a sector boundary or start/finish boundary. Timing values are stored in seconds and exposed on each car snapshot as `lapTelemetry`.
63
+
62
64
  Each car is marked `finished` when it reaches `track.length * totalLaps`. The first finisher becomes `winner`, receives classified rank `1`, and emits a `car-finish` event, but the race keeps running until every car has crossed the finish distance.
63
65
 
64
66
  When all cars have finished, the simulator records `finishedAt` and final `classification`, emits a `race-finish` event, freezes order to the classified result, deploys the safety car, and switches race mode to `safety-car`. Finished cars keep circulating under safety-car behavior instead of hard-stopping. The current implementation does not yet implement pit stops, penalties, or championship scoring.
@@ -73,9 +73,18 @@ simulator.mountTimingTower(timingRoot);
73
73
  simulator.mountRaceCanvas(canvasRoot, {
74
74
  includeRaceDataPanel: true,
75
75
  includeTimingTower: true,
76
+ includeTelemetrySectorBanner: true,
76
77
  timingTowerVerticalFit: 'scroll',
77
78
  });
78
79
  simulator.mountTelemetryPanel(telemetryRoot);
80
+ simulator.mountTelemetryCore(coreTelemetryRoot);
81
+ simulator.mountTelemetrySectors(sectorGraphRoot);
82
+ simulator.mountTelemetrySectorBanner(sectorBannerRoot);
83
+ simulator.mountTelemetryLapTimes(lapTimesRoot);
84
+ simulator.mountTelemetrySectorTimes(sectorTimesRoot);
85
+ simulator.mountRaceTelemetryDrawer(raceWorkbenchRoot, {
86
+ timingTowerVerticalFit: 'expand-race-view',
87
+ });
79
88
 
80
89
  await simulator.start();
81
90
  ```
@@ -88,9 +97,16 @@ mountTimingTower(root, simulator);
88
97
  mountRaceCanvas(root, simulator, {
89
98
  includeRaceDataPanel: true,
90
99
  includeTimingTower: true,
100
+ includeTelemetrySectorBanner: true,
91
101
  timingTowerVerticalFit: 'scroll',
92
102
  });
93
103
  mountTelemetryPanel(root, simulator);
104
+ mountTelemetryCore(root, simulator);
105
+ mountTelemetrySectors(root, simulator);
106
+ mountTelemetrySectorBanner(root, simulator);
107
+ mountTelemetryLapTimes(root, simulator);
108
+ mountTelemetrySectorTimes(root, simulator);
109
+ mountRaceTelemetryDrawer(root, simulator);
94
110
  mountRaceDataPanel(root, simulator);
95
111
  ```
96
112
 
@@ -103,8 +119,14 @@ Returned controller:
103
119
  mountCameraControls(root),
104
120
  mountSafetyCarControl(root),
105
121
  mountTimingTower(root),
106
- mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, timingTowerVerticalFit }),
122
+ mountRaceCanvas(root, { includeRaceDataPanel, includeTimingTower, includeTelemetrySectorBanner, timingTowerVerticalFit }),
107
123
  mountTelemetryPanel(root),
124
+ mountTelemetryCore(root),
125
+ mountTelemetrySectors(root),
126
+ mountTelemetrySectorBanner(root),
127
+ mountTelemetryLapTimes(root),
128
+ mountTelemetrySectorTimes(root),
129
+ mountRaceTelemetryDrawer(root, { timingTowerVerticalFit, drawerInitiallyOpen }),
108
130
  mountRaceDataPanel(root),
109
131
  start(),
110
132
 
@@ -131,18 +153,20 @@ Returned controller:
131
153
  - Timing tower entries are fixed rows stacked from the top of the timing list. Row vertical position must be based on rank/index, never distributed by available height or total entry count.
132
154
  - Timing tower entries display team icons. The tower can switch at runtime between interval-to-car-ahead timing and cumulative gap-to-leader timing.
133
155
  - Snapshots expose calibrated display units through `speedKph`, `distanceMeters`, and `gapMeters`; internal physics remains in simulator units.
156
+ - Snapshots expose automatic three-sector track metadata through `track.sectors` and per-car lap/sector timing through `car.lapTelemetry`, including sector performance classes for overall-best, personal-best, and slower completed sector times.
134
157
  - Entries can include optional `team` metadata. Team color defaults to car color when omitted.
135
158
  - Mounted package surfaces show a package-owned red start-light loading overlay until `start()` finishes PixiJS, asset, control, and initial readout initialization.
136
159
  - `preset` is a preset-first API. Presets are resolved before explicit host overrides so hosts can use `dashboard`, `timing-overlay`, `compact-race`, or `full-dashboard` as a starting point and still override specific `ui` or `theme` fields.
137
160
  - `theme` is the public sizing/color contract. It maps to package CSS variables for `accentColor`, `greenColor`, `yellowColor`, `timingTowerMaxWidth`, and `raceViewMinHeight`.
138
161
  - Camera controls can be embedded in the race canvas, externally mounted, or omitted by `ui.cameraControls`.
139
- - Telemetry can include the car/driver overview directly or stay text-only through `ui.telemetryIncludesOverview` / `mountTelemetryPanel(root, { includeOverview: false })`.
162
+ - Telemetry surfaces are detached package components: core scalar readouts, sector graph, broadcast sector banner, lap-time table, and sector-time table. The broadcast sector banner shows the selected car identity, uses the selected car color for its frame/label, and keeps sector performance colors inside the sector bars. `mountTelemetryPanel()` is only a stack template around those detached pieces, and `ui.telemetryModules` controls which pieces appear in stack/drawer templates.
163
+ - `mountRaceTelemetryDrawer()` creates a package-owned race workbench: race canvas, embedded timing tower, safety-car control, lower-third banner, and a right-side telemetry drawer. The drawer opens smoothly, takes width from the race view instead of overlaying it, and is inert/hidden to interaction when closed.
140
164
  - The FPS readout can be shown or hidden with `ui.showFps`.
141
165
  - `ui.layoutPreset: 'left-tower-overlay'` is a package-owned preset that creates a left broadcast gutter inside the race view, places the timing tower there at the same width as the default timing-board column, frames the PixiJS camera around the remaining usable race area, and keeps camera controls and start lights out of the tower area. In the combined shell, project and radio lower-thirds stay inside the race window while being allowed to cover the timing sidebar.
142
166
  - `ui.raceDataBanners.initial` controls the starting lower-third (`'project'`, `'radio'`, or `'hidden'`), and `ui.raceDataBanners.enabled` controls which lower-third types can appear.
143
167
  - `ui.raceDataBannerSize` controls lower-third sizing: `'custom'` keeps the default CSS-variable-driven banner size for host tuning, while `'auto'` uses the race space to the right of the timing board when wide enough and overlaps the timing board only when space is constrained.
144
168
  - `ui.timingTowerVerticalFit` controls vertical tower behavior in the combined overlay preset: `'expand-race-view'` grows the race window to fit the tower, while `'scroll'` crops the tower area and scrolls timing rows inside it. The same values are accepted as `mountRaceCanvas()` options when `includeTimingTower` embeds the tower in the race canvas.
145
- - Hosts may scale the whole mounted simulator through the container. The horizontal proportions inside package-owned presets are not public API and should not be configurable through raw width or ratio options. The camera reads the current canvas dimensions so wider or taller host windows reveal more of the race view without needing host-owned camera math. The timing tower has a package-owned max width because overly wide timing boards degrade readability; standalone hosts can constrain vertical height through the mount container and let the timing entries scroll internally.
169
+ - Hosts may scale the whole mounted simulator through the container. The horizontal proportions inside package-owned presets are not public API and should not be configurable through raw width or ratio options. The camera reads the current canvas dimensions so wider or taller host windows reveal more of the race view without needing host-owned camera math. The timing tower has a package-owned max width because overly wide timing boards degrade readability; standalone hosts can constrain vertical height through the mount container and let the timing entries scroll internally. Mobile and narrow embeds are handled by package CSS: timing boards stack full-width when they no longer work as side gutters, camera controls are repositioned into the remaining race area, and full-width timing boards do not reserve horizontal camera gutter space.
146
170
  - The host does not need to provide simulator assets.
147
171
  - The host passes data, not internal DOM.
148
172
  - `onDriverOpen(driver)` is the navigation boundary.
@@ -153,7 +177,7 @@ Returned controller:
153
177
  - The simulation should remain deterministic for the same seed, track seed, drivers, entries, and rules.
154
178
  - When `trackSeed` is omitted in a browser mount, the simulator creates a fresh procedural circuit for that mount. Explicit `trackSeed` values are deterministic and cached by seed for repeated mounts.
155
179
  - The renderer should target a paced 60 FPS simulation/render loop.
156
- - The render loop should pause while the race canvas is offscreen or the document is hidden, then resume without catching up the elapsed hidden time. Layout measurements needed for overlay camera safe areas should be cached between resize/layout invalidations.
180
+ - The render loop should pause while the race canvas is offscreen or the document is hidden, then resume without catching up the elapsed hidden time. Layout measurements needed for overlay camera safe areas should be cached between resize/layout invalidations. Runtime DOM updates should skip unchanged text/markup so visible embeds do not rewrite stable readouts every frame.
157
181
 
158
182
  ## Current Visible Features
159
183
 
@@ -169,7 +193,7 @@ Returned controller:
169
193
  - Safety car toggle.
170
194
  - External safety-car control through controller methods and optional mounted button.
171
195
  - Restart button.
172
- - Selected-car text telemetry.
196
+ - Detached selected-car telemetry components for scalar readouts, sector progress graph, lap timing table, and sector timing table.
173
197
  - Car/driver overview panel with a center visual, linked stat cells, and a Car/Driver toggle.
174
198
  - Project/race data lower-third.
175
199
  - Intermittent project radio quotes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inventure71/paddockjs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "A data-driven browser F1 simulator component for project portfolios.",
6
6
  "license": "Apache-2.0",
@@ -5,8 +5,14 @@ import {
5
5
  createRaceCanvasMarkup,
6
6
  createRaceControlsMarkup,
7
7
  createRaceDataPanelMarkup,
8
+ createRaceTelemetryDrawerMarkup,
8
9
  createSafetyCarControlMarkup,
10
+ createTelemetryCoreMarkup,
11
+ createTelemetryLapTimesMarkup,
9
12
  createTelemetryPanelMarkup,
13
+ createTelemetrySectorBannerMarkup,
14
+ createTelemetrySectorTimesMarkup,
15
+ createTelemetrySectorsMarkup,
10
16
  createTimingTowerMarkup,
11
17
  } from '../ui/componentTemplates.js';
12
18
  import { applyPaddockThemeCssVariables, resolveF1SimulatorOptions } from '../config/defaultOptions.js';
@@ -117,18 +123,44 @@ export class PaddockSimulatorController {
117
123
  mountRaceCanvas(root, {
118
124
  includeRaceDataPanel = false,
119
125
  includeTimingTower = false,
126
+ includeTelemetrySectorBanner = false,
120
127
  timingTowerVerticalFit,
121
128
  } = {}) {
122
129
  return this.mountComponent(root, 'race-canvas', createRaceCanvasMarkup({
123
130
  ...this.options,
124
131
  includeRaceDataPanel,
125
132
  includeTimingTower,
133
+ includeTelemetrySectorBanner,
126
134
  timingTowerVerticalFit,
127
135
  }));
128
136
  }
129
137
 
130
138
  mountTelemetryPanel(root, { includeOverview } = {}) {
131
- return this.mountComponent(root, 'telemetry-panel', createTelemetryPanelMarkup(this.options, { includeOverview }));
139
+ return this.mountComponent(root, 'telemetry-stack', createTelemetryPanelMarkup(this.options, { includeOverview }));
140
+ }
141
+
142
+ mountTelemetryCore(root) {
143
+ return this.mountComponent(root, 'telemetry-core', createTelemetryCoreMarkup(this.options));
144
+ }
145
+
146
+ mountTelemetrySectors(root) {
147
+ return this.mountComponent(root, 'telemetry-sectors', createTelemetrySectorsMarkup(this.options));
148
+ }
149
+
150
+ mountTelemetryLapTimes(root) {
151
+ return this.mountComponent(root, 'telemetry-lap-times', createTelemetryLapTimesMarkup(this.options));
152
+ }
153
+
154
+ mountTelemetrySectorTimes(root) {
155
+ return this.mountComponent(root, 'telemetry-sector-times', createTelemetrySectorTimesMarkup(this.options));
156
+ }
157
+
158
+ mountTelemetrySectorBanner(root) {
159
+ return this.mountComponent(root, 'telemetry-sector-banner', createTelemetrySectorBannerMarkup(this.options));
160
+ }
161
+
162
+ mountRaceTelemetryDrawer(root, options = {}) {
163
+ return this.mountComponent(root, 'race-telemetry-drawer', createRaceTelemetryDrawerMarkup(this.options, options));
132
164
  }
133
165
 
134
166
  mountCarDriverOverview(root) {
@@ -228,6 +260,30 @@ export function mountTelemetryPanel(root, simulator, options) {
228
260
  return simulator.mountTelemetryPanel(root, options);
229
261
  }
230
262
 
263
+ export function mountTelemetryCore(root, simulator) {
264
+ return simulator.mountTelemetryCore(root);
265
+ }
266
+
267
+ export function mountTelemetrySectors(root, simulator) {
268
+ return simulator.mountTelemetrySectors(root);
269
+ }
270
+
271
+ export function mountTelemetryLapTimes(root, simulator) {
272
+ return simulator.mountTelemetryLapTimes(root);
273
+ }
274
+
275
+ export function mountTelemetrySectorTimes(root, simulator) {
276
+ return simulator.mountTelemetrySectorTimes(root);
277
+ }
278
+
279
+ export function mountTelemetrySectorBanner(root, simulator) {
280
+ return simulator.mountTelemetrySectorBanner(root);
281
+ }
282
+
283
+ export function mountRaceTelemetryDrawer(root, simulator, options) {
284
+ return simulator.mountRaceTelemetryDrawer(root, options);
285
+ }
286
+
231
287
  export function mountRaceDataPanel(root, simulator) {
232
288
  return simulator.mountRaceDataPanel(root);
233
289
  }