@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
@@ -1,4 +1,6 @@
1
1
  import { nearestTrackState } from '../../simulation/track/trackModel.js';
2
+ import { normalizeAngle } from '../../simulation/simMath.js';
3
+ import { offsetTrackPoint, pointAt, sampleIndexAtDistance } from '../../simulation/track/spatialQueries.js';
2
4
  import { pitOverrideAllowedForCar } from '../../simulation/track/trackStatePolicy.js';
3
5
  import { metersToSimUnits, simUnitsToMeters } from '../../simulation/units.js';
4
6
  import {
@@ -12,13 +14,18 @@ import { canUseIndexedRecoveryRayApproximation } from './rayGuards.js';
12
14
  import { degreesToRadians, getCarRayOrigin, getCarRayVector, pointOnRay } from './rayGeometry.js';
13
15
  import { findIndexedTrackBandBoundaries } from './indexedRayBands.js';
14
16
 
17
+ const RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE = metersToSimUnits(1);
18
+ const RAY_TRACK_STATE_REUSE_PROGRESS_TOLERANCE = metersToSimUnits(0.25);
19
+ const RAY_TRACK_STATE_REUSE_HEADING_TOLERANCE = 0.02;
20
+
15
21
  export function createTrackRayContext(car, snapshot, origin, precision = 'debug') {
16
22
  if (!Array.isArray(snapshot.track?.samples) || snapshot.track.samples.length === 0) {
17
23
  return { origin, originState: null, precision };
18
24
  }
25
+ const reusedOriginState = reusableRayOriginState(snapshot.track, car, origin);
19
26
  return {
20
27
  origin,
21
- originState: nearestRayTrackState(snapshot.track, car, origin, car.progress, precision),
28
+ originState: reusedOriginState ?? nearestRayTrackState(snapshot.track, car, origin, car.progress, precision),
22
29
  precision,
23
30
  };
24
31
  }
@@ -43,6 +50,18 @@ export function estimateTrackHit(
43
50
  ? context.originState
44
51
  : nearestRayTrackState(snapshot.track, car, origin, car.progress, precision);
45
52
  const includePitLane = Boolean(car.inPitLane || car.pitLanePart || originState.inPitLane);
53
+ const indexedHit = estimateIndexedTrackHit({
54
+ car,
55
+ track: snapshot.track,
56
+ origin,
57
+ originState,
58
+ ray,
59
+ lengthMeters,
60
+ includePitLane,
61
+ precision,
62
+ sharedRayQuery,
63
+ });
64
+ if (indexedHit) return indexedHit;
46
65
  const directionalAnalyticHit = canUseDirectionalAnalyticTrackHit({ car, track: snapshot.track, originState, ray, includePitLane })
47
66
  ? estimateAnalyticMainTrackHit({
48
67
  car,
@@ -55,20 +74,6 @@ export function estimateTrackHit(
55
74
  })
56
75
  : null;
57
76
  if (directionalAnalyticHit) return directionalAnalyticHit;
58
- if (!usesSampledDriverContract(car, precision)) {
59
- const indexedHit = estimateIndexedTrackHit({
60
- car,
61
- track: snapshot.track,
62
- origin,
63
- originState,
64
- ray,
65
- lengthMeters,
66
- includePitLane,
67
- precision,
68
- sharedRayQuery,
69
- });
70
- if (indexedHit) return indexedHit;
71
- }
72
77
  const analyticHit = estimateAnalyticMainTrackHit({
73
78
  car,
74
79
  track: snapshot.track,
@@ -168,7 +173,6 @@ function canUseDirectionalAnalyticTrackHit({ car, track, originState, ray, inclu
168
173
 
169
174
  function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthMeters, includePitLane, precision, sharedRayQuery }) {
170
175
  if (includePitLane || (!usesMainTrackOnlyRays(car) && isNearPitConnector(track, originState))) return null;
171
- if (!canUseIndexedRecoveryRayApproximation(track, originState)) return null;
172
176
  const trackHalfWidth = track.width / 2;
173
177
  const inside = Math.abs(originState.signedOffset ?? 0) <= trackHalfWidth;
174
178
  const boundaries = findIndexedTrackBandBoundaries(
@@ -181,21 +185,28 @@ function estimateIndexedTrackHit({ car, track, origin, originState, ray, lengthM
181
185
  sharedRayQuery,
182
186
  );
183
187
  if (!boundaries.available) return null;
184
- if (boundaries.trackEdgeDistance == null) return null;
188
+ if (boundaries.trackEdgeDistance == null) {
189
+ if (usesMainTrackOnlyRays(car)) return createTrackMiss(lengthMeters);
190
+ return canSampleRecoveryTransition(track, originState, ray)
191
+ ? null
192
+ : createTrackMiss(lengthMeters);
193
+ }
185
194
  const kind = inside ? 'exit' : 'entry';
186
- const hitDistance = validatedIndexedTransitionDistance({
187
- track,
188
- car,
189
- origin,
190
- ray,
191
- progressHint: car.progress,
192
- distance: boundaries.trackEdgeDistance,
193
- maxDistance: metersToSimUnits(lengthMeters),
194
- kind,
195
- includePitLane,
196
- precision,
197
- originInside: inside,
198
- });
195
+ const hitDistance = usesMainTrackOnlyRays(car)
196
+ ? boundaries.trackEdgeDistance
197
+ : validatedIndexedTransitionDistance({
198
+ track,
199
+ car,
200
+ origin,
201
+ ray,
202
+ progressHint: car.progress,
203
+ distance: boundaries.trackEdgeDistance,
204
+ maxDistance: metersToSimUnits(lengthMeters),
205
+ kind,
206
+ includePitLane,
207
+ precision,
208
+ originInside: inside,
209
+ });
199
210
  if (hitDistance == null) return null;
200
211
  return {
201
212
  hit: true,
@@ -287,16 +298,116 @@ function refineTrackTransitionDistance(
287
298
  }
288
299
 
289
300
  function nearestRayTrackState(track, car, point, progressHint, precision = 'debug') {
290
- const useBatchTrainingIndexMode = usesMainTrackOnlyRays(car);
291
301
  return nearestTrackState(track, point, progressHint, {
292
302
  allowPitOverride: pitOverrideAllowedForCar(car),
293
- indexMode: usesSampledDriverContract(car, precision) ? 'legacy' : useBatchTrainingIndexMode ? 'sample' : undefined,
294
- hintMaxDistance: useBatchTrainingIndexMode ? Infinity : undefined,
295
303
  });
296
304
  }
297
305
 
298
- function usesSampledDriverContract(car, precision) {
299
- return precision === 'driver' && !usesMainTrackOnlyRays(car);
306
+ function reusableRayOriginState(track, car, origin) {
307
+ const synthesizedState = synthesizedMainRoadRayOriginState(track, car, origin);
308
+ if (synthesizedState) return synthesizedState;
309
+ const trackState = car?.trackState;
310
+ if (!trackState || trackState.inPitLane) return null;
311
+ const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
312
+ if (!Number.isFinite(trackState.crossTrackError) || trackState.crossTrackError > mainRoadEdge) return null;
313
+ if (
314
+ typeof car?.trackStatePoseX === 'number' &&
315
+ typeof car?.trackStatePoseY === 'number' &&
316
+ typeof car?.trackStatePoseHeading === 'number'
317
+ ) {
318
+ if (
319
+ car.trackStatePoseX !== car.x ||
320
+ car.trackStatePoseY !== car.y ||
321
+ car.trackStatePoseHeading !== car.heading
322
+ ) {
323
+ return null;
324
+ }
325
+ if (!Number.isInteger(trackState.segmentId) && Number.isFinite(trackState.distance)) {
326
+ trackState.segmentId = sampleIndexAtDistance(track, trackState.distance);
327
+ }
328
+ } else {
329
+ if (!Number.isFinite(trackState.distance) || !Number.isFinite(trackState.signedOffset)) return null;
330
+ const base = pointAt(track, trackState.distance);
331
+ const projected = offsetTrackPoint(base, trackState.signedOffset);
332
+ const dx = (origin?.x ?? car?.x ?? 0) - projected.x;
333
+ const dy = (origin?.y ?? car?.y ?? 0) - projected.y;
334
+ if ((dx * dx) + (dy * dy) > RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE * RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE) {
335
+ return null;
336
+ }
337
+ if (wrappedDistanceDelta(trackState.distance, car?.progress, track.length) > RAY_TRACK_STATE_REUSE_PROGRESS_TOLERANCE) {
338
+ return null;
339
+ }
340
+ if (Math.abs(normalizeAngle((trackState.heading ?? base.heading) - base.heading)) > RAY_TRACK_STATE_REUSE_HEADING_TOLERANCE) {
341
+ return null;
342
+ }
343
+ const segmentId = trackState.segmentId ?? sampleIndexAtDistance(track, trackState.distance);
344
+ return {
345
+ segmentId,
346
+ x: base.x,
347
+ y: base.y,
348
+ distance: base.distance,
349
+ heading: base.heading,
350
+ normalX: base.normalX,
351
+ normalY: base.normalY,
352
+ curvature: base.curvature,
353
+ signedOffset: trackState.signedOffset,
354
+ crossTrackError: trackState.crossTrackError,
355
+ surface: trackState.surface ?? 'track',
356
+ onTrack: trackState.surface === 'track' || trackState.surface === 'kerb',
357
+ distanceSquared: trackState.distanceSquared,
358
+ inPitLane: false,
359
+ pitLanePart: null,
360
+ pitBoxId: null,
361
+ mainTrackSignedOffset: trackState.mainTrackSignedOffset,
362
+ mainTrackCrossTrackError: trackState.mainTrackCrossTrackError,
363
+ pitLaneCrossTrackError: trackState.pitLaneCrossTrackError,
364
+ };
365
+ }
366
+ return trackState;
367
+ }
368
+
369
+ function synthesizedMainRoadRayOriginState(track, car, origin) {
370
+ if (!Number.isFinite(car?.progress) || !Number.isFinite(car?.signedOffset)) return null;
371
+ if (car?.inPitLane || car?.pitLanePart) return null;
372
+ const crossTrackError = Math.abs(car.signedOffset);
373
+ const mainRoadEdge = track.width / 2 + (track.kerbWidth ?? 0);
374
+ if (crossTrackError > mainRoadEdge) return null;
375
+ const base = pointAt(track, car.progress);
376
+ const projected = offsetTrackPoint(base, car.signedOffset);
377
+ const dx = (origin?.x ?? car?.x ?? 0) - projected.x;
378
+ const dy = (origin?.y ?? car?.y ?? 0) - projected.y;
379
+ if ((dx * dx) + (dy * dy) > RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE * RAY_TRACK_STATE_REUSE_POSITION_TOLERANCE) {
380
+ return null;
381
+ }
382
+ const segmentId = car?.trackState?.segmentId ?? sampleIndexAtDistance(track, car.progress);
383
+ return {
384
+ segmentId,
385
+ x: base.x,
386
+ y: base.y,
387
+ distance: base.distance,
388
+ heading: base.heading,
389
+ normalX: base.normalX,
390
+ normalY: base.normalY,
391
+ curvature: base.curvature,
392
+ signedOffset: car.signedOffset,
393
+ crossTrackError,
394
+ surface: crossTrackError <= track.width / 2 ? 'track' : 'kerb',
395
+ onTrack: true,
396
+ distanceSquared: car?.trackState?.distanceSquared,
397
+ inPitLane: false,
398
+ pitLanePart: null,
399
+ pitBoxId: null,
400
+ mainTrackSignedOffset: car?.trackState?.mainTrackSignedOffset,
401
+ mainTrackCrossTrackError: car?.trackState?.mainTrackCrossTrackError,
402
+ pitLaneCrossTrackError: car?.trackState?.pitLaneCrossTrackError,
403
+ };
404
+ }
405
+
406
+ function wrappedDistanceDelta(first, second, totalLength) {
407
+ if (!Number.isFinite(first) || !Number.isFinite(second)) return Infinity;
408
+ if (!Number.isFinite(totalLength) || totalLength <= 0) return Math.abs(first - second);
409
+ const delta = Math.abs(first - second);
410
+ return Math.min(delta, totalLength - delta);
300
411
  }
301
412
 
302
413
  function refineStepsForPrecision(precision) {
@@ -308,6 +419,16 @@ function isInsideTrackBorder(state, track, includePitLane = true) {
308
419
  return state.crossTrackError <= track.width / 2;
309
420
  }
310
421
 
422
+ function canSampleRecoveryTransition(track, originState, ray) {
423
+ if (!canUseIndexedRecoveryRayApproximation(track, originState)) return false;
424
+ const trackHalfWidth = track.width / 2;
425
+ const offset = originState.signedOffset ?? 0;
426
+ if (Math.abs(offset) <= trackHalfWidth) return false;
427
+ const lateral = ray.x * originState.normalX + ray.y * originState.normalY;
428
+ if (Math.abs(lateral) < 0.05) return false;
429
+ return offset > 0 ? lateral < 0 : lateral > 0;
430
+ }
431
+
311
432
  function validatedIndexedTransitionDistance({
312
433
  track,
313
434
  car,
@@ -323,19 +444,8 @@ function validatedIndexedTransitionDistance({
323
444
  }) {
324
445
  const step = metersToSimUnits(TRACK_RAY_STEP_METERS);
325
446
  const refineSteps = refineStepsForPrecision(precision);
326
- if (refineSteps <= 0 && usesMainTrackOnlyRays(car)) return distance;
327
447
  if (refineSteps <= 0) {
328
- return sampledIndexedTransitionDistance({
329
- track,
330
- car,
331
- origin,
332
- ray,
333
- progressHint,
334
- distance,
335
- maxDistance,
336
- kind,
337
- includePitLane,
338
- });
448
+ return distance;
339
449
  }
340
450
 
341
451
  const windows = [step * 1.5, step * 8];
@@ -418,7 +528,6 @@ function refineValidatedTransitionDistance(track, car, origin, ray, progressHint
418
528
  function validatedInsideTrackBorder(track, car, origin, ray, distance, progressHint, includePitLane) {
419
529
  const state = nearestTrackState(track, pointOnRay(origin, ray, distance), progressHint, {
420
530
  allowPitOverride: pitOverrideAllowedForCar(car),
421
- indexMode: 'legacy',
422
531
  });
423
532
  return isInsideTrackBorder(state, track, includePitLane);
424
533
  }
package/src/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type {
2
2
  PaddockActionSpec,
3
3
  PaddockDriverControllerLoop,
4
4
  PaddockDriverControllerLoopOptions,
5
+ PaddockEnvironmentOptions,
5
6
  PaddockObservationSpec,
6
7
  PaddockParticipantInteraction as EnvPaddockParticipantInteraction,
7
8
  PaddockParticipantInteractionsOptions as EnvPaddockParticipantInteractionsOptions,
@@ -23,7 +24,7 @@ export type {
23
24
  } from './environment/index.js';
24
25
 
25
26
  export type TireCompound = 'S' | 'M' | 'H';
26
- export type PaddockPhysicsMode = 'arcade' | 'simulator';
27
+ export type PaddockPhysicsMode = 'arcade' | 'advanced';
27
28
  export type PaddockWarmupPolicy = 'config-change' | 'always' | 'never';
28
29
  export type PaddockProceduralTrackProfile = 'race' | 'training-short' | 'training-medium' | 'training-technical';
29
30
 
@@ -81,11 +82,12 @@ export type PaddockPitIntentRequest = PaddockPitIntent | {
81
82
  pitTargetCompound?: TireCompound | string;
82
83
  targetTire?: TireCompound | string;
83
84
  };
84
- export type CameraMode = 'overview' | 'leader' | 'selected' | 'show-all' | 'pit';
85
+ export type CameraMode = 'overview' | 'leader' | 'selected' | 'driver' | 'show-all' | 'pit';
85
86
  export type RaceBannerMode = 'project' | 'radio' | 'hidden';
86
87
  export type RaceBannerEnabledMode = 'project' | 'radio';
87
88
  export type RaceDataBannerSize = 'auto' | 'custom';
88
89
  export type TimingTowerVerticalFit = 'expand-race-view' | 'scroll';
90
+ export type TimingGapMode = 'interval' | 'leader';
89
91
  export type LayoutPreset = 'standard' | 'left-tower-overlay';
90
92
  export type CameraControlsMode = 'embedded' | 'external' | false;
91
93
  export type PaddockPresetName = 'dashboard' | 'timing-overlay' | 'compact-race' | 'full-dashboard';
@@ -111,6 +113,7 @@ export interface TeamData {
111
113
  name?: string;
112
114
  color?: string;
113
115
  icon?: string;
116
+ theme?: PaddockThemeSelector;
114
117
  pitCrew?: PaddockPitCrewStats;
115
118
  pitCrewStats?: PaddockPitCrewStats;
116
119
  }
@@ -254,12 +257,110 @@ export interface NormalizedSimulatorDriver extends SimulatorDriver {
254
257
  };
255
258
  }
256
259
 
257
- export interface F1SimulatorTheme {
258
- accentColor?: string;
259
- greenColor?: string;
260
- yellowColor?: string;
261
- timingTowerMaxWidth?: string;
262
- raceViewMinHeight?: string;
260
+ export type PaddockThemeMode = 'dark' | 'light' | 'system';
261
+ export type PaddockThemeSelector = 'default' | 'active' | 'selectedTeam' | 'team' | `team:${string}` | string;
262
+ export type PaddockThemeTokenValue = string | { light?: string; dark?: string };
263
+ export type PaddockThemeTokenName =
264
+ | 'primary'
265
+ | 'primaryText'
266
+ | 'secondary'
267
+ | 'secondaryText'
268
+ | 'surface'
269
+ | 'surfaceRaised'
270
+ | 'surfacePanel'
271
+ | 'text'
272
+ | 'mutedText'
273
+ | 'border'
274
+ | 'success'
275
+ | 'warning'
276
+ | 'danger'
277
+ | 'info'
278
+ | 'yellowFlag'
279
+ | 'greenFlag'
280
+ | 'redFlag'
281
+ | 'safetyCar'
282
+ | 'drsActive'
283
+ | 'pitLane'
284
+ | 'track'
285
+ | 'trackEdge'
286
+ | 'timingTowerMaxWidth'
287
+ | 'raceViewMinHeight';
288
+
289
+ export interface F1SimulatorThemeTokens extends Partial<Record<PaddockThemeTokenName, PaddockThemeTokenValue>> {
290
+ /** Compatibility alias for `primary`. Prefer `tokens.primary` in new 3.0.0 code. */
291
+ accentColor?: PaddockThemeTokenValue;
292
+ /** Compatibility alias for `greenFlag`. Prefer `tokens.greenFlag` or `tokens.success`. */
293
+ greenColor?: PaddockThemeTokenValue;
294
+ /** Compatibility alias for `yellowFlag`. Prefer `tokens.yellowFlag` or `tokens.warning`. */
295
+ yellowColor?: PaddockThemeTokenValue;
296
+ /** Compatibility alias for `info`. */
297
+ blueColor?: PaddockThemeTokenValue;
298
+ /** Compatibility alias for `redFlag`. Prefer `tokens.redFlag` or `tokens.danger`. */
299
+ raceControlRedColor?: PaddockThemeTokenValue;
300
+ /** Compatibility alias for `surface`. */
301
+ surfaceColor?: PaddockThemeTokenValue;
302
+ /** Compatibility alias for `surfaceRaised`. */
303
+ surfaceRaisedColor?: PaddockThemeTokenValue;
304
+ /** Compatibility alias for `surfacePanel`. */
305
+ surfacePanelColor?: PaddockThemeTokenValue;
306
+ /** Compatibility alias for `border`. */
307
+ lineColor?: PaddockThemeTokenValue;
308
+ /** Compatibility alias for `border`. */
309
+ lineStrongColor?: PaddockThemeTokenValue;
310
+ /** Compatibility alias for `text`. */
311
+ textColor?: PaddockThemeTokenValue;
312
+ /** Compatibility alias for `mutedText`. */
313
+ mutedTextColor?: PaddockThemeTokenValue;
314
+ /** Compatibility alias for `track`. */
315
+ trackColor?: PaddockThemeTokenValue;
316
+ /** Compatibility alias for `trackEdge`. */
317
+ trackEdgeColor?: PaddockThemeTokenValue;
318
+ }
319
+
320
+ export interface F1SimulatorThemePackage extends F1SimulatorThemeTokens {
321
+ extends?: string;
322
+ tokens?: F1SimulatorThemeTokens;
323
+ components?: Record<string, Partial<Record<string, PaddockThemeTokenName>>>;
324
+ light?: F1SimulatorThemeTokens;
325
+ dark?: F1SimulatorThemeTokens;
326
+ }
327
+
328
+ export interface F1SimulatorTheme extends Omit<F1SimulatorThemePackage, 'components'> {
329
+ mode?: PaddockThemeMode;
330
+ use?: string;
331
+ themes?: Record<string, F1SimulatorThemePackage>;
332
+ componentThemes?: Record<string, PaddockThemeSelector | { theme: PaddockThemeSelector }>;
333
+ teamThemes?: Record<string, PaddockThemeSelector | { theme: PaddockThemeSelector }>;
334
+ components?: Record<
335
+ string,
336
+ PaddockThemeSelector | { theme: PaddockThemeSelector } | Partial<Record<string, PaddockThemeTokenName>>
337
+ >;
338
+ }
339
+
340
+ export type ResolvedPaddockThemeMode = Exclude<PaddockThemeMode, 'system'>;
341
+
342
+ export interface ResolvedPaddockThemePackage {
343
+ tokens: Record<ResolvedPaddockThemeMode, F1SimulatorThemeTokens>;
344
+ components: Record<string, Partial<Record<string, PaddockThemeTokenName>>>;
345
+ }
346
+
347
+ export interface ResolvedPaddockTheme {
348
+ mode: PaddockThemeMode;
349
+ activeMode: ResolvedPaddockThemeMode;
350
+ use: string;
351
+ tokens: Record<ResolvedPaddockThemeMode, F1SimulatorThemeTokens>;
352
+ activeTokens: F1SimulatorThemeTokens;
353
+ components: Record<string, Partial<Record<string, PaddockThemeTokenName>>>;
354
+ themes: Record<string, ResolvedPaddockThemePackage>;
355
+ componentThemes: Record<string, PaddockThemeSelector>;
356
+ teamThemes: Record<string, PaddockThemeSelector>;
357
+ light: F1SimulatorThemeTokens;
358
+ dark: F1SimulatorThemeTokens;
359
+ }
360
+
361
+ export interface PaddockThemeSyncOptions {
362
+ attribute?: string;
363
+ map?: Record<string, PaddockThemeMode | F1SimulatorTheme>;
263
364
  }
264
365
 
265
366
  export interface F1SimulatorUiOptions {
@@ -277,10 +378,15 @@ export interface F1SimulatorUiOptions {
277
378
  };
278
379
  raceDataBannerSize?: RaceDataBannerSize;
279
380
  raceDataTelemetryDetail?: boolean;
381
+ driverCamera?: boolean;
280
382
  penaltyBanners?: boolean;
281
383
  timingPenaltyBadges?: boolean;
282
384
  simulationSpeedControl?: boolean;
283
385
  timingTowerVerticalFit?: TimingTowerVerticalFit;
386
+ timingGapMode?: TimingGapMode;
387
+ timingGapModeToggle?: boolean;
388
+ timingEntryVerticalPadding?: number;
389
+ responsiveNarrowLayout?: boolean;
284
390
  }
285
391
 
286
392
  export interface F1SimulatorDebugOptions {
@@ -638,6 +744,18 @@ export interface WheelSurfaceSnapshot {
638
744
  fullyOutsideWhiteLine: boolean;
639
745
  }
640
746
 
747
+ export interface CarTrackStateSnapshot {
748
+ distance: number;
749
+ signedOffset: number;
750
+ crossTrackError: number;
751
+ surface: string;
752
+ inPitLane: boolean;
753
+ pitLanePart: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
754
+ pitBoxId: string | null;
755
+ curvature: number;
756
+ heading: number;
757
+ }
758
+
641
759
  export interface CarSnapshot {
642
760
  id: string;
643
761
  rank: number | null;
@@ -683,6 +801,7 @@ export interface CarSnapshot {
683
801
  stabilityState?: PaddockStabilityState;
684
802
  signedOffset?: number;
685
803
  crossTrackError?: number;
804
+ trackState?: CarTrackStateSnapshot | null;
686
805
  inPitLane?: boolean;
687
806
  wheels?: WheelSurfaceSnapshot[];
688
807
  pitLanePart?: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
@@ -791,6 +910,7 @@ export interface F1SimulatorExpertOptions {
791
910
  enabled: boolean;
792
911
  controlledDrivers: string[];
793
912
  frameSkip?: number;
913
+ episode?: PaddockEnvironmentOptions['episode'];
794
914
  visualizeSensors?: boolean | {
795
915
  rays?: boolean;
796
916
  drivers?: 'selected' | 'all' | string[];
@@ -862,7 +982,6 @@ export interface F1SimulatorOptions extends F1SimulatorCallbacks {
862
982
  seed?: number;
863
983
  trackSeed?: number;
864
984
  trackGeneration?: PaddockProceduralTrackOptions;
865
- trackQueryIndex?: boolean;
866
985
  warmup?: PaddockWarmupOptions | boolean;
867
986
  totalLaps?: number;
868
987
  physicsMode?: PaddockPhysicsMode;
@@ -889,6 +1008,7 @@ export interface MountRaceCanvasOptions {
889
1008
  includeTimingTower?: boolean;
890
1009
  includeTelemetrySectorBanner?: boolean;
891
1010
  timingTowerVerticalFit?: TimingTowerVerticalFit;
1011
+ responsiveNarrowLayout?: boolean;
892
1012
  }
893
1013
 
894
1014
  export interface MountTelemetryPanelOptions {
@@ -899,12 +1019,17 @@ export interface MountRaceTelemetryDrawerOptions {
899
1019
  timingTowerVerticalFit?: TimingTowerVerticalFit;
900
1020
  drawerInitiallyOpen?: boolean;
901
1021
  raceDataTelemetryDetail?: boolean;
1022
+ responsiveNarrowLayout?: boolean;
902
1023
  }
903
1024
 
904
1025
  export interface F1MountedSimulator {
905
1026
  readonly expert: F1SimulatorExpertApi | null;
906
1027
  destroy(): void;
907
1028
  restart(nextOptions?: F1SimulatorRestartOptions): void;
1029
+ setTheme(theme: F1SimulatorTheme): ResolvedPaddockTheme;
1030
+ setThemeMode(mode: PaddockThemeMode): ResolvedPaddockTheme;
1031
+ getTheme(): ResolvedPaddockTheme;
1032
+ syncThemeFrom(source: Element, options?: PaddockThemeSyncOptions): () => void;
908
1033
  selectDriver(driverId: string): void;
909
1034
  setSafetyCarDeployed(deployed: boolean): void;
910
1035
  setRedFlagDeployed(deployed: boolean): void;
@@ -916,6 +1041,9 @@ export interface F1MountedSimulator {
916
1041
  getPitIntent(driverId: string): PaddockPitIntent;
917
1042
  getPitTargetCompound(driverId: string): TireCompound | string | null;
918
1043
  getSimulationSpeed(): number;
1044
+ setTimingGapMode(mode: TimingGapMode): TimingGapMode;
1045
+ getTimingGapMode(): TimingGapMode;
1046
+ toggleTimingGapMode(): TimingGapMode;
919
1047
  servePenalty(penaltyId: string): PaddockPenaltyEntry | null;
920
1048
  cancelPenalty(penaltyId: string): PaddockPenaltyEntry | null;
921
1049
  getSnapshot(): RaceSnapshot | null;
@@ -942,6 +1070,10 @@ export interface PaddockSimulatorController {
942
1070
  start(): Promise<PaddockSimulatorController>;
943
1071
  destroy(): void;
944
1072
  restart(nextOptions?: F1SimulatorRestartOptions): void;
1073
+ setTheme(theme: F1SimulatorTheme): ResolvedPaddockTheme;
1074
+ setThemeMode(mode: PaddockThemeMode): ResolvedPaddockTheme;
1075
+ getTheme(): ResolvedPaddockTheme;
1076
+ syncThemeFrom(source: Element, options?: PaddockThemeSyncOptions): () => void;
945
1077
  selectDriver(driverId: string): void;
946
1078
  setSafetyCarDeployed(deployed: boolean): void;
947
1079
  setRedFlagDeployed(deployed: boolean): void;
@@ -953,6 +1085,9 @@ export interface PaddockSimulatorController {
953
1085
  getPitIntent(driverId: string): PaddockPitIntent;
954
1086
  getPitTargetCompound(driverId: string): TireCompound | string | null;
955
1087
  getSimulationSpeed(): number;
1088
+ setTimingGapMode(mode: TimingGapMode): TimingGapMode;
1089
+ getTimingGapMode(): TimingGapMode;
1090
+ toggleTimingGapMode(): TimingGapMode;
956
1091
  servePenalty(penaltyId: string): PaddockPenaltyEntry | null;
957
1092
  cancelPenalty(penaltyId: string): PaddockPenaltyEntry | null;
958
1093
  getSnapshot(): RaceSnapshot | null;
@@ -961,10 +1096,13 @@ export interface PaddockSimulatorController {
961
1096
  export const CHAMPIONSHIP_ENTRY_BLUEPRINTS: ChampionshipEntryBlueprint[];
962
1097
  export const DEMO_PROJECT_DRIVERS: SimulatorDriver[];
963
1098
  export const DEFAULT_F1_SIMULATOR_ASSETS: Required<F1SimulatorAssets>;
1099
+ export const DEFAULT_PADDOCK_THEME: F1SimulatorTheme;
964
1100
  export const PADDOCK_SIMULATOR_PRESETS: Record<PaddockPresetName, {
965
1101
  ui?: F1SimulatorUiOptions;
966
1102
  theme?: F1SimulatorTheme;
967
1103
  }>;
1104
+ export const PADDOCK_THEME_CSS_VARIABLES: Record<PaddockThemeTokenName, string>;
1105
+ export const PADDOCK_THEME_TOKEN_KEYS: readonly PaddockThemeTokenName[];
968
1106
 
969
1107
  export const REAL_F1_CAR_LENGTH_METERS: number;
970
1108
  export const SIM_UNITS_PER_METER: number;
@@ -980,6 +1118,12 @@ export function normalizeSimulatorDrivers(
980
1118
  drivers: SimulatorDriver[],
981
1119
  options?: { entries?: ChampionshipEntryBlueprint[]; caller?: string },
982
1120
  ): NormalizedSimulatorDriver[];
1121
+ export function resolvePaddockTheme(theme?: F1SimulatorTheme): ResolvedPaddockTheme;
1122
+ export function applyPaddockTheme(
1123
+ root: Element,
1124
+ theme?: F1SimulatorTheme | ResolvedPaddockTheme,
1125
+ context?: { selectedTeamId?: string | null },
1126
+ ): void;
983
1127
 
984
1128
  export function metersToSimUnits(meters: number): number;
985
1129
  export function simUnitsToMeters(simUnits: number): number;
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ import './styles.css';
2
2
  import { F1SimulatorApp } from './app/F1SimulatorApp.js';
3
3
  import { resolveF1SimulatorOptions } from './config/defaultOptions.js';
4
4
  import { mergeRestartOptions } from './config/restartOptions.js';
5
+ import { createThemeSync, resolveRuntimeThemeModeOptions, resolveRuntimeThemeOptions } from './api/runtimeTheme.js';
5
6
  import { createF1SimulatorShell } from './ui/shellTemplate.js';
6
7
  export {
7
8
  createPaddockSimulator,
@@ -30,7 +31,14 @@ export {
30
31
  } from './data/championship.js';
31
32
  export { DEMO_PROJECT_DRIVERS } from './data/demoDrivers.js';
32
33
  export { DEFAULT_F1_SIMULATOR_ASSETS } from './config/defaultAssets.js';
33
- export { PADDOCK_SIMULATOR_PRESETS } from './config/defaultOptions.js';
34
+ export {
35
+ DEFAULT_PADDOCK_THEME,
36
+ PADDOCK_SIMULATOR_PRESETS,
37
+ PADDOCK_THEME_CSS_VARIABLES,
38
+ PADDOCK_THEME_TOKEN_KEYS,
39
+ applyPaddockTheme,
40
+ resolvePaddockTheme,
41
+ } from './config/defaultOptions.js';
34
42
  export { createPaddockDriverControllerLoop } from './environment/controllerLoop.js';
35
43
  export { normalizeSimulatorDrivers } from './data/normalizeDrivers.js';
36
44
  export { createProceduralTrack } from './simulation/track/trackModel.js';
@@ -61,7 +69,22 @@ export async function mountF1Simulator(root, options = {}) {
61
69
  const app = new F1SimulatorApp(shell, resolvedOptions);
62
70
  await app.init();
63
71
 
64
- return {
72
+ const syncResolvedTimingGapMode = (timingGapMode) => {
73
+ resolvedOptions = {
74
+ ...resolvedOptions,
75
+ ui: {
76
+ ...resolvedOptions.ui,
77
+ timingGapMode,
78
+ },
79
+ };
80
+ return timingGapMode;
81
+ };
82
+ const syncResolvedRuntimeOptions = () => {
83
+ syncResolvedTimingGapMode(app.getTimingGapMode());
84
+ return resolvedOptions;
85
+ };
86
+
87
+ const mountedSimulator = {
65
88
  get expert() {
66
89
  return app.expert ?? null;
67
90
  },
@@ -70,10 +93,26 @@ export async function mountF1Simulator(root, options = {}) {
70
93
  root.innerHTML = '';
71
94
  },
72
95
  restart(nextOptions = {}) {
73
- const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(resolvedOptions, nextOptions));
96
+ const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(syncResolvedRuntimeOptions(), nextOptions));
74
97
  app.restart(nextResolvedOptions);
75
98
  resolvedOptions = nextResolvedOptions;
76
99
  },
100
+ setTheme(themeInput = {}) {
101
+ const nextResolvedOptions = resolveRuntimeThemeOptions(syncResolvedRuntimeOptions(), themeInput);
102
+ resolvedOptions = nextResolvedOptions;
103
+ return app.setTheme(nextResolvedOptions.theme);
104
+ },
105
+ setThemeMode(mode) {
106
+ const nextResolvedOptions = resolveRuntimeThemeModeOptions(syncResolvedRuntimeOptions(), mode);
107
+ resolvedOptions = nextResolvedOptions;
108
+ return app.setTheme(nextResolvedOptions.theme);
109
+ },
110
+ getTheme() {
111
+ return resolvedOptions.theme;
112
+ },
113
+ syncThemeFrom(source, options = {}) {
114
+ return createThemeSync(mountedSimulator, source, options);
115
+ },
77
116
  selectDriver(driverId) {
78
117
  app.selectCar(driverId, { focus: true });
79
118
  },
@@ -108,6 +147,15 @@ export async function mountF1Simulator(root, options = {}) {
108
147
  getSimulationSpeed() {
109
148
  return app.simulationSpeed;
110
149
  },
150
+ setTimingGapMode(mode) {
151
+ return syncResolvedTimingGapMode(app.setTimingGapMode(mode));
152
+ },
153
+ getTimingGapMode() {
154
+ return app.getTimingGapMode();
155
+ },
156
+ toggleTimingGapMode() {
157
+ return syncResolvedTimingGapMode(app.toggleTimingGapMode());
158
+ },
111
159
  servePenalty(penaltyId) {
112
160
  return app.servePenalty(penaltyId);
113
161
  },
@@ -118,4 +166,5 @@ export async function mountF1Simulator(root, options = {}) {
118
166
  return app.getSnapshot();
119
167
  },
120
168
  };
169
+ return mountedSimulator;
121
170
  }