@inventure71/paddockjs 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/INSTALL_AND_UPDATE.md +10 -1
  3. package/README.md +65 -15
  4. package/docs/architecture.md +17 -13
  5. package/docs/component_inventory.md +72 -0
  6. package/docs/concepts.md +2 -2
  7. package/docs/custom_model_controller.md +2 -2
  8. package/docs/data_contract.md +96 -21
  9. package/docs/index.md +2 -1
  10. package/docs/learnings.md +6 -6
  11. package/docs/rules.md +7 -7
  12. package/docs/sense_contract.md +1 -1
  13. package/docs/system_specs.md +25 -18
  14. package/docs/training.md +13 -12
  15. package/package.json +1 -1
  16. package/src/api/PaddockSimulatorController.js +74 -4
  17. package/src/app/F1SimulatorApp.js +191 -28
  18. package/src/app/banners/raceDataBannerController.js +71 -36
  19. package/src/app/banners/stewardMessageController.js +5 -4
  20. package/src/app/camera/cameraConstants.js +2 -0
  21. package/src/app/camera/cameraController.js +86 -29
  22. package/src/app/camera/cameraGeometry.js +12 -1
  23. package/src/app/domBindings.js +89 -9
  24. package/src/app/layoutScheduler.js +27 -0
  25. package/src/app/layoutSupport.js +165 -0
  26. package/src/app/raceOverlayClearanceSupport.js +158 -0
  27. package/src/app/readouts/carOverviewRenderer.js +16 -8
  28. package/src/app/readouts/controlStateRenderer.js +1 -1
  29. package/src/app/readouts/raceStatusRenderer.js +30 -21
  30. package/src/app/readouts/readoutFormatters.js +2 -0
  31. package/src/app/readouts/telemetryRenderer.js +17 -8
  32. package/src/app/readouts/timingTowerRenderer.js +62 -10
  33. package/src/app/runtime/runtimeVisibility.js +66 -5
  34. package/src/config/cssValues.js +16 -0
  35. package/src/config/defaultOptions.js +51 -38
  36. package/src/config/themeOptions.js +825 -0
  37. package/src/config/timingGapMode.js +13 -0
  38. package/src/config/urlOptions.js +25 -0
  39. package/src/data/championship.js +14 -9
  40. package/src/environment/controllerLoop.js +17 -3
  41. package/src/environment/index.d.ts +1 -2
  42. package/src/environment/options.js +1 -1
  43. package/src/environment/runtime.js +3 -11
  44. package/src/environment/scenarios.js +1 -1
  45. package/src/environment/sensors/surfaceRays.js +41 -33
  46. package/src/environment/sensors/trackRays.js +44 -49
  47. package/src/index.d.ts +110 -9
  48. package/src/index.js +25 -1
  49. package/src/simulation/driver/driverController.js +1 -1
  50. package/src/simulation/driver/edgeRecovery.js +1 -1
  51. package/src/simulation/driver/racingControls.js +2 -2
  52. package/src/simulation/driver/rejoinControls.js +2 -2
  53. package/src/simulation/pit/pitKinematics.js +2 -2
  54. package/src/simulation/pit/pitRouting.js +2 -4
  55. package/src/simulation/race/raceSetup.js +2 -6
  56. package/src/simulation/race/raceStep.js +1 -1
  57. package/src/simulation/race/safetyCar.js +1 -1
  58. package/src/simulation/race/stalledDnf.js +1 -1
  59. package/src/simulation/timing/lapTelemetry.js +28 -9
  60. package/src/simulation/track/pitLaneLayout.js +7 -3
  61. package/src/simulation/track/spatialQueries.js +5 -28
  62. package/src/simulation/track/trackQueryIndex.js +180 -135
  63. package/src/simulation/track/trackStatePolicy.js +0 -1
  64. package/src/simulation/vehicle/contactResolution.js +3 -3
  65. package/src/simulation/vehicle/vehicleKinematics.js +2 -2
  66. package/src/simulation/vehicle/vehiclePhysics.js +3 -3
  67. package/src/simulation/vehicle/vehicleSnapshots.js +12 -1
  68. package/src/simulation/vehicle/vehicleState.js +1 -1
  69. package/src/simulation/warmup/runtimeWarmup.js +0 -1
  70. package/src/styles.css +1185 -136
  71. package/src/ui/bannerTemplates.js +38 -28
  72. package/src/ui/cameraControlsTemplate.js +16 -5
  73. package/src/ui/carOverviewTemplate.js +12 -5
  74. package/src/ui/raceCanvasTemplate.js +31 -8
  75. package/src/ui/raceControlsTemplate.js +27 -10
  76. package/src/ui/shellTemplate.js +3 -1
  77. package/src/ui/telemetryDrawerTemplate.js +23 -10
  78. package/src/ui/telemetryTemplates.js +58 -32
  79. package/src/ui/templateUtils.js +48 -0
  80. package/src/ui/timingTowerTemplate.js +29 -12
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,84 @@ 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
+ >;
263
338
  }
264
339
 
265
340
  export interface F1SimulatorUiOptions {
@@ -277,10 +352,15 @@ export interface F1SimulatorUiOptions {
277
352
  };
278
353
  raceDataBannerSize?: RaceDataBannerSize;
279
354
  raceDataTelemetryDetail?: boolean;
355
+ driverCamera?: boolean;
280
356
  penaltyBanners?: boolean;
281
357
  timingPenaltyBadges?: boolean;
282
358
  simulationSpeedControl?: boolean;
283
359
  timingTowerVerticalFit?: TimingTowerVerticalFit;
360
+ timingGapMode?: TimingGapMode;
361
+ timingGapModeToggle?: boolean;
362
+ timingEntryVerticalPadding?: number;
363
+ responsiveNarrowLayout?: boolean;
284
364
  }
285
365
 
286
366
  export interface F1SimulatorDebugOptions {
@@ -638,6 +718,18 @@ export interface WheelSurfaceSnapshot {
638
718
  fullyOutsideWhiteLine: boolean;
639
719
  }
640
720
 
721
+ export interface CarTrackStateSnapshot {
722
+ distance: number;
723
+ signedOffset: number;
724
+ crossTrackError: number;
725
+ surface: string;
726
+ inPitLane: boolean;
727
+ pitLanePart: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
728
+ pitBoxId: string | null;
729
+ curvature: number;
730
+ heading: number;
731
+ }
732
+
641
733
  export interface CarSnapshot {
642
734
  id: string;
643
735
  rank: number | null;
@@ -683,6 +775,7 @@ export interface CarSnapshot {
683
775
  stabilityState?: PaddockStabilityState;
684
776
  signedOffset?: number;
685
777
  crossTrackError?: number;
778
+ trackState?: CarTrackStateSnapshot | null;
686
779
  inPitLane?: boolean;
687
780
  wheels?: WheelSurfaceSnapshot[];
688
781
  pitLanePart?: 'entry' | 'fast-lane' | 'working-lane' | 'exit' | 'service-box' | 'garage-box' | null;
@@ -791,6 +884,7 @@ export interface F1SimulatorExpertOptions {
791
884
  enabled: boolean;
792
885
  controlledDrivers: string[];
793
886
  frameSkip?: number;
887
+ episode?: PaddockEnvironmentOptions['episode'];
794
888
  visualizeSensors?: boolean | {
795
889
  rays?: boolean;
796
890
  drivers?: 'selected' | 'all' | string[];
@@ -862,7 +956,6 @@ export interface F1SimulatorOptions extends F1SimulatorCallbacks {
862
956
  seed?: number;
863
957
  trackSeed?: number;
864
958
  trackGeneration?: PaddockProceduralTrackOptions;
865
- trackQueryIndex?: boolean;
866
959
  warmup?: PaddockWarmupOptions | boolean;
867
960
  totalLaps?: number;
868
961
  physicsMode?: PaddockPhysicsMode;
@@ -889,6 +982,7 @@ export interface MountRaceCanvasOptions {
889
982
  includeTimingTower?: boolean;
890
983
  includeTelemetrySectorBanner?: boolean;
891
984
  timingTowerVerticalFit?: TimingTowerVerticalFit;
985
+ responsiveNarrowLayout?: boolean;
892
986
  }
893
987
 
894
988
  export interface MountTelemetryPanelOptions {
@@ -899,6 +993,7 @@ export interface MountRaceTelemetryDrawerOptions {
899
993
  timingTowerVerticalFit?: TimingTowerVerticalFit;
900
994
  drawerInitiallyOpen?: boolean;
901
995
  raceDataTelemetryDetail?: boolean;
996
+ responsiveNarrowLayout?: boolean;
902
997
  }
903
998
 
904
999
  export interface F1MountedSimulator {
@@ -916,6 +1011,9 @@ export interface F1MountedSimulator {
916
1011
  getPitIntent(driverId: string): PaddockPitIntent;
917
1012
  getPitTargetCompound(driverId: string): TireCompound | string | null;
918
1013
  getSimulationSpeed(): number;
1014
+ setTimingGapMode(mode: TimingGapMode): TimingGapMode;
1015
+ getTimingGapMode(): TimingGapMode;
1016
+ toggleTimingGapMode(): TimingGapMode;
919
1017
  servePenalty(penaltyId: string): PaddockPenaltyEntry | null;
920
1018
  cancelPenalty(penaltyId: string): PaddockPenaltyEntry | null;
921
1019
  getSnapshot(): RaceSnapshot | null;
@@ -953,6 +1051,9 @@ export interface PaddockSimulatorController {
953
1051
  getPitIntent(driverId: string): PaddockPitIntent;
954
1052
  getPitTargetCompound(driverId: string): TireCompound | string | null;
955
1053
  getSimulationSpeed(): number;
1054
+ setTimingGapMode(mode: TimingGapMode): TimingGapMode;
1055
+ getTimingGapMode(): TimingGapMode;
1056
+ toggleTimingGapMode(): TimingGapMode;
956
1057
  servePenalty(penaltyId: string): PaddockPenaltyEntry | null;
957
1058
  cancelPenalty(penaltyId: string): PaddockPenaltyEntry | null;
958
1059
  getSnapshot(): RaceSnapshot | null;
package/src/index.js CHANGED
@@ -61,6 +61,21 @@ export async function mountF1Simulator(root, options = {}) {
61
61
  const app = new F1SimulatorApp(shell, resolvedOptions);
62
62
  await app.init();
63
63
 
64
+ const syncResolvedTimingGapMode = (timingGapMode) => {
65
+ resolvedOptions = {
66
+ ...resolvedOptions,
67
+ ui: {
68
+ ...resolvedOptions.ui,
69
+ timingGapMode,
70
+ },
71
+ };
72
+ return timingGapMode;
73
+ };
74
+ const syncResolvedRuntimeOptions = () => {
75
+ syncResolvedTimingGapMode(app.getTimingGapMode());
76
+ return resolvedOptions;
77
+ };
78
+
64
79
  return {
65
80
  get expert() {
66
81
  return app.expert ?? null;
@@ -70,7 +85,7 @@ export async function mountF1Simulator(root, options = {}) {
70
85
  root.innerHTML = '';
71
86
  },
72
87
  restart(nextOptions = {}) {
73
- const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(resolvedOptions, nextOptions));
88
+ const nextResolvedOptions = resolveF1SimulatorOptions(mergeRestartOptions(syncResolvedRuntimeOptions(), nextOptions));
74
89
  app.restart(nextResolvedOptions);
75
90
  resolvedOptions = nextResolvedOptions;
76
91
  },
@@ -108,6 +123,15 @@ export async function mountF1Simulator(root, options = {}) {
108
123
  getSimulationSpeed() {
109
124
  return app.simulationSpeed;
110
125
  },
126
+ setTimingGapMode(mode) {
127
+ return syncResolvedTimingGapMode(app.setTimingGapMode(mode));
128
+ },
129
+ getTimingGapMode() {
130
+ return app.getTimingGapMode();
131
+ },
132
+ toggleTimingGapMode() {
133
+ return syncResolvedTimingGapMode(app.toggleTimingGapMode());
134
+ },
111
135
  servePenalty(penaltyId) {
112
136
  return app.servePenalty(penaltyId);
113
137
  },
@@ -37,7 +37,7 @@ export function decideDriverControls({ car, orderIndex, race }) {
37
37
  }
38
38
 
39
39
  function shouldStabilizeSimulatorCar(car, race) {
40
- if (race.physicsMode !== 'simulator') return false;
40
+ if (race.physicsMode !== 'advanced') return false;
41
41
  const gripUsage = car.gripUsage ?? 0;
42
42
  const slipAngle = Math.abs(car.slipAngleRadians ?? 0);
43
43
  const edgeDistance = race.track.width / 2 - VEHICLE_LIMITS.carWidth * 2.25;
@@ -5,7 +5,7 @@ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
5
5
  import { REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX } from './driverControlConstants.js';
6
6
 
7
7
  export function calculateTrackEdgeGuard(car, race) {
8
- const simulatorMode = race.physicsMode === 'simulator';
8
+ const simulatorMode = race.physicsMode === 'advanced';
9
9
  const trackLimit = race.track.width / 2;
10
10
  const halfCarWidth = VEHICLE_LIMITS.carWidth / 2;
11
11
  const wholeCarOutsideLimit = trackLimit + halfCarWidth;
@@ -10,7 +10,7 @@ import { calculateActualOverlapPenalty, calculatePlannedTrafficPenalty, planRaci
10
10
  import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
11
11
 
12
12
  export function decideRacingControls(car, orderIndex, race) {
13
- if (race.physicsMode === 'simulator') {
13
+ if (race.physicsMode === 'advanced') {
14
14
  return decideSimulatorRacingControls(car, orderIndex, race);
15
15
  }
16
16
  return decideArcadeRacingControls(car, orderIndex, race);
@@ -389,7 +389,7 @@ export function calculateRacingLineOffset(car, race, lookahead, curvature, edgeG
389
389
  if (turnDirection === 0) return 0;
390
390
 
391
391
  const aggression = car.aggression ?? car.personality?.baseAggression ?? 0.5;
392
- const simulatorMode = race.physicsMode === 'simulator';
392
+ const simulatorMode = race.physicsMode === 'advanced';
393
393
  const safeEdge = race.track.width / 2 - VEHICLE_LIMITS.carWidth *
394
394
  (simulatorMode ? 2.05 - aggression * 0.08 : 1.15 - aggression * 0.12);
395
395
  const apexOffset = turnDirection * safeEdge *
@@ -8,7 +8,7 @@ import { VEHICLE_LIMITS } from '../vehicle/vehiclePhysics.js';
8
8
  import { analyzeTrackEdgeMotion } from './recoveryDynamics.js';
9
9
 
10
10
  export function decideRejoinControls(car, race) {
11
- if (race.physicsMode === 'simulator') {
11
+ if (race.physicsMode === 'advanced') {
12
12
  return decideSimulatorRejoinControls(car, race);
13
13
  }
14
14
  return decideArcadeRejoinControls(car, race);
@@ -103,7 +103,7 @@ export function decideSimulatorRejoinControls(car, race) {
103
103
  }
104
104
 
105
105
  function decideRejoinControlsForMode(car, race, profile) {
106
- const simulatorMode = race.physicsMode === 'simulator';
106
+ const simulatorMode = race.physicsMode === 'advanced';
107
107
  const lookahead = clamp(car.speed * (profile.simulatorMode ? 0.58 : 0.72) + REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_BASE, REJOIN_LOOKAHEAD_MAX);
108
108
  const edgeMotion = analyzeTrackEdgeMotion(car, race);
109
109
  const lowSpeedOffTrack = !car.trackState.onTrack && car.speed < kphToSimSpeed(24);
@@ -1,7 +1,7 @@
1
- export function syncPitCarKinematics(car, { physicsMode = 'simulator' } = {}) {
1
+ export function syncPitCarKinematics(car, { physicsMode = 'arcade' } = {}) {
2
2
  const speed = Number.isFinite(car.speed) ? car.speed : 0;
3
3
  const heading = Number.isFinite(car.heading) ? car.heading : 0;
4
- if (physicsMode === 'simulator') {
4
+ if (physicsMode === 'advanced') {
5
5
  car.velocityX = Math.cos(heading) * speed;
6
6
  car.velocityY = Math.sin(heading) * speed;
7
7
  } else {
@@ -285,11 +285,9 @@ export function nearestDistanceOnRoute(route, position, previousDistance = 0) {
285
285
  export function createPitApproachPoints(track, car, pitLane, entryRaceDistance) {
286
286
  const currentDistance = car.raceDistance ?? entryRaceDistance - PIT_ENTRY_APPROACH_DISTANCE;
287
287
  const remaining = Math.max(0, entryRaceDistance - currentDistance);
288
- const currentState = nearestTrackState(track, car, car.progress, { indexMode: 'legacy' });
288
+ const currentState = nearestTrackState(track, car, car.progress);
289
289
  const startOffset = currentState.inPitLane ? 0 : currentState.signedOffset ?? 0;
290
- const entryConnectState = nearestTrackState(track, pitLane.entry.trackConnectPoint, pitLane.entry.trackDistance, {
291
- indexMode: 'legacy',
292
- });
290
+ const entryConnectState = nearestTrackState(track, pitLane.entry.trackConnectPoint, pitLane.entry.trackDistance);
293
291
  const targetOffset = entryConnectState.signedOffset ?? 0;
294
292
  const points = [routePoint(car, car.heading)];
295
293
  const straightDistance = remaining > metersToSimUnits(14)
@@ -29,7 +29,6 @@ export function initializeRaceSimulation(simulation, {
29
29
  track = null,
30
30
  trackSeed = null,
31
31
  trackGeneration = {},
32
- trackQueryIndex = true,
33
32
  physicsMode = 'arcade',
34
33
  participantInteractions = {},
35
34
  replayGhosts = [],
@@ -43,11 +42,8 @@ export function initializeRaceSimulation(simulation, {
43
42
  ...builtTrack,
44
43
  pitLane: clonePitLaneModel(builtTrack.pitLane),
45
44
  };
46
- const useTrackQueryIndex = trackQueryIndex !== false;
47
- if (useTrackQueryIndex) {
48
- const forkedCachedIndex = forkTrackQueryIndex(builtTrack.queryIndex);
49
- attachTrackQueryIndex(simulation.track, forkedCachedIndex ?? createTrackQueryIndex(simulation.track));
50
- }
45
+ const forkedCachedIndex = forkTrackQueryIndex(builtTrack.queryIndex, simulation.track);
46
+ attachTrackQueryIndex(simulation.track, forkedCachedIndex ?? createTrackQueryIndex(simulation.track));
51
47
  simulation.track.timingLines = createTimingLines(simulation.track);
52
48
  simulation.trackSeed = simulation.track.seed ?? trackSeed;
53
49
  simulation.rules = normalizeRaceRules(rules);
@@ -59,7 +59,7 @@ export function runRaceStep(simulation, dt) {
59
59
  throttle: controls.throttle ?? 0,
60
60
  brake: controls.brake ?? 0,
61
61
  };
62
- if (simulation.physicsMode === 'simulator') {
62
+ if (simulation.physicsMode === 'advanced') {
63
63
  applyWheelSurfaceState(car, simulation.track);
64
64
  }
65
65
  integrateVehiclePhysics(car, controls, delta, {
@@ -82,7 +82,7 @@ export function setRedFlagState(sim, deployed) {
82
82
  });
83
83
  } else if (!car.finished) {
84
84
  car.speed = Math.max(car.speed, kphToSimSpeed(60));
85
- if (sim.physicsMode === 'simulator') syncVehicleVelocityToHeading(car);
85
+ if (sim.physicsMode === 'advanced') syncVehicleVelocityToHeading(car);
86
86
  car.brake = 0;
87
87
  car.throttle = Math.max(car.throttle ?? 0, 0.35);
88
88
  }
@@ -59,7 +59,7 @@ function retireStalledCar(sim, car) {
59
59
  });
60
60
  }
61
61
 
62
- export function freezeRetiredCar(car, { physicsMode = 'simulator' } = {}) {
62
+ export function freezeRetiredCar(car, { physicsMode = 'arcade' } = {}) {
63
63
  freezeVehicleMotion(car, {
64
64
  clearManualControls: true,
65
65
  physicsMode,
@@ -33,9 +33,10 @@ function getLapTelemetryPosition(track, raceDistance, totalLaps = Infinity) {
33
33
  };
34
34
  }
35
35
 
36
- function createSectorProgress(position, currentSectors = null) {
36
+ function createSectorProgress(position, currentSectors = null, { preserveCompletedSectors = false } = {}) {
37
37
  const activeIndex = clamp((position.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
38
38
  return createEmptySectorTimes().map((_, index) => {
39
+ if (preserveCompletedSectors && Number.isFinite(currentSectors?.[index])) return 1;
39
40
  if (index < activeIndex && Number.isFinite(currentSectors?.[index])) return 1;
40
41
  if (index === activeIndex) return clamp(position.currentSectorProgress ?? 0, 0, 1);
41
42
  return 0;
@@ -99,11 +100,23 @@ export function updateLapTelemetry(car, previousRaceDistance, currentTime, track
99
100
  const previousUpdateTime = Number.isFinite(telemetry.lastUpdatedAt) ? telemetry.lastUpdatedAt : currentTime;
100
101
  const elapsedTime = Math.max(0, currentTime - previousUpdateTime);
101
102
 
102
- if (!Number.isFinite(previousDistance) || !Number.isFinite(currentDistance) || travelled < -1e-3 || travelled > track.length / 2) {
103
+ if (
104
+ !Number.isFinite(previousDistance) ||
105
+ !Number.isFinite(currentDistance) ||
106
+ Math.abs(travelled) > track.length / 2
107
+ ) {
103
108
  resetLapTelemetry(car, currentTime, track, totalLaps);
104
109
  return;
105
110
  }
106
111
 
112
+ if (travelled < -1e-3) {
113
+ syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps, {
114
+ preserveCompletedSectors: true,
115
+ });
116
+ telemetry.lastUpdatedAt = currentTime;
117
+ return;
118
+ }
119
+
107
120
  const sectorLength = getSectorLength(track);
108
121
  if (travelled > 1e-6) {
109
122
  const firstBoundary = Math.floor(previousDistance / sectorLength) + 1;
@@ -155,28 +168,34 @@ function clearFutureSectorTelemetry(telemetry) {
155
168
  }
156
169
  }
157
170
 
158
- function syncLiveSectorTelemetry(telemetry) {
171
+ function syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors = false } = {}) {
159
172
  const activeIndex = clamp((telemetry.currentSector ?? 1) - 1, 0, TELEMETRY_SECTOR_COUNT - 1);
160
173
 
161
174
  telemetry.liveSectors = createEmptySectorTimes();
162
175
  telemetry.currentSectors.forEach((time, index) => {
163
- if (index >= activeIndex) return;
176
+ if (!preserveCompletedSectors && index >= activeIndex) return;
164
177
  if (!Number.isFinite(time)) return;
165
178
  telemetry.liveSectors[index] = time;
166
179
  });
167
- telemetry.liveSectors[activeIndex] = telemetry.currentSectorElapsed;
180
+ if (!Number.isFinite(telemetry.liveSectors[activeIndex])) {
181
+ telemetry.liveSectors[activeIndex] = telemetry.currentSectorElapsed;
182
+ }
168
183
  }
169
184
 
170
- function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps) {
185
+ function syncLapTelemetryPosition(telemetry, currentTime, currentRaceDistance, track, totalLaps, {
186
+ preserveCompletedSectors = false,
187
+ } = {}) {
171
188
  const position = getLapTelemetryPosition(track, currentRaceDistance, totalLaps);
172
189
  telemetry.completedLaps = position.completedLaps;
173
190
  telemetry.currentLap = position.currentLap;
174
191
  telemetry.currentSector = position.currentSector;
175
192
  telemetry.currentSectorProgress = position.currentSectorProgress;
176
- clearFutureSectorTelemetry(telemetry);
177
- telemetry.sectorProgress = createSectorProgress(position, telemetry.currentSectors);
193
+ if (!preserveCompletedSectors) clearFutureSectorTelemetry(telemetry);
194
+ telemetry.sectorProgress = createSectorProgress(position, telemetry.currentSectors, {
195
+ preserveCompletedSectors,
196
+ });
178
197
  telemetry.completedLaps = Math.max(telemetry.completedLaps, Math.min(position.completedLaps, totalLaps));
179
198
  telemetry.currentLapTime = Math.max(0, currentTime - telemetry.currentLapStartedAt);
180
199
  telemetry.currentSectorElapsed = Math.max(0, currentTime - telemetry.currentSectorStartedAt);
181
- syncLiveSectorTelemetry(telemetry);
200
+ syncLiveSectorTelemetry(telemetry, { preserveCompletedSectors });
182
201
  }
@@ -1,9 +1,9 @@
1
1
  import { clamp } from '../simMath.js';
2
2
  import { metersToSimUnits } from '../units.js';
3
3
  import { WORLD, PIT_BOX_COUNT, PIT_BOX_DEPTH, PIT_BOX_LENGTH, PIT_BOX_PAIR_GAP, PIT_BOX_TO_LANE_GAP, PIT_ENTRY_SEARCH_AFTER, PIT_ENTRY_SEARCH_BEFORE, PIT_EXIT_SEARCH_AFTER, PIT_EXIT_SEARCH_BEFORE, PIT_LANE_EDGE_GAP, PIT_LANE_ENTRY_BUFFER, PIT_LANE_EXIT_BUFFER, PIT_LANE_FINISH_RATIO, PIT_LANE_OFFSET_SEARCH_STEP, PIT_LANE_WIDTH, PIT_SERVICE_AREA_DEPTH, PIT_SERVICE_AREA_LENGTH, PIT_SERVICE_QUEUE_GAP, PIT_TEAM_COUNT, PIT_TEAM_GAP, PIT_BOXES_PER_TEAM, PIT_WORKING_LANE_GAP, PIT_WORKING_LANE_WIDTH, PIT_ACCESS_MAX_LENGTH, PIT_ACCESS_MIN_LENGTH, PIT_ACCESS_TANGENT_RATIO, PIT_TRACK_CLEARANCE_MARGIN } from './trackConstants.js';
4
- import { clonePoint, distance, headingVector, interpolatePitPoint, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
4
+ import { clonePoint, distance, headingVector, interpolatePitPoint, nearestPointOnPolyline, normalizeVector, pointWorldClearance, projectPitPoint, createPointBounds } from './trackMath.js';
5
5
  import { findPitAccessConnection, createPitAccessRoadCenterline } from './pitLaneAccess.js';
6
- import { nearestTrackState, pointAt } from './spatialQueries.js';
6
+ import { pointAt } from './spatialQueries.js';
7
7
 
8
8
  export function createStartStraightBasis(track) {
9
9
  const finish = pointAt(track, 0);
@@ -90,7 +90,7 @@ export function scorePitLanePlacement(track, side, laneOffset, layout) {
90
90
  minimumClearance = Math.min(minimumClearance, pointWorldClearance(lanePoint), pointWorldClearance(boxPoint));
91
91
  minimumTrackClearance = Math.min(
92
92
  minimumTrackClearance,
93
- nearestTrackState(track, lanePoint).crossTrackError -
93
+ nearestMainTrackProjection(track, lanePoint).crossTrackError -
94
94
  (track.width / 2 + (track.kerbWidth ?? 0) + PIT_TRACK_CLEARANCE_MARGIN),
95
95
  );
96
96
  }
@@ -105,6 +105,10 @@ export function scorePitLanePlacement(track, side, laneOffset, layout) {
105
105
  };
106
106
  }
107
107
 
108
+ function nearestMainTrackProjection(track, position) {
109
+ return nearestPointOnPolyline(track.samples, position) ?? { crossTrackError: Infinity };
110
+ }
111
+
108
112
  export function choosePitLanePlacement(track, baseLaneOffset, layout) {
109
113
  const candidates = [];
110
114
  for (let offsetStep = 0; offsetStep <= 9; offsetStep += 1) {
@@ -1,5 +1,5 @@
1
1
  import { clamp, normalizeAngle, wrapDistance } from '../simMath.js';
2
- import { NEAREST_HINT_WINDOW_SAMPLES, PIT_LANE_WIDTH } from './trackConstants.js';
2
+ import { PIT_LANE_WIDTH } from './trackConstants.js';
3
3
  import { nearestPitLaneState } from './pitLaneState.js';
4
4
  import { queryNearestTrackProjection } from './trackQueryIndex.js';
5
5
 
@@ -91,13 +91,10 @@ export function createTrackState(track, position, best) {
91
91
 
92
92
  export function nearestTrackState(track, position, progressHint = null, options = {}) {
93
93
  const allowPitOverride = options.allowPitOverride !== false;
94
- const best = options.indexMode === 'legacy'
95
- ? nearestTrackSampleLegacy(track, position, progressHint)
96
- : queryNearestTrackProjection(track, position, progressHint, {
97
- indexMode: options.indexMode,
98
- hintMaxDistance: options.hintMaxDistance,
99
- }) ??
100
- nearestTrackSampleLegacy(track, position, progressHint);
94
+ const best = queryNearestTrackProjection(track, position, progressHint);
95
+ if (!best) {
96
+ throw new Error('nearestTrackState requires a finite position and an indexed track model. Build tracks with buildTrackModel() or createRaceSimulation().');
97
+ }
101
98
  const trackState = createTrackState(track, position, best);
102
99
  if (!allowPitOverride) return trackState;
103
100
  const pitState = nearestPitLaneState(track, position);
@@ -125,26 +122,6 @@ export function nearestTrackState(track, position, progressHint = null, options
125
122
  };
126
123
  }
127
124
 
128
- function nearestTrackSampleLegacy(track, position, progressHint = null) {
129
- let nearest = null;
130
-
131
- if (Number.isFinite(progressHint)) {
132
- const centerIndex = sampleIndexAtDistance(track, progressHint);
133
- nearest = nearestSampleInRange(
134
- track,
135
- position,
136
- centerIndex - NEAREST_HINT_WINDOW_SAMPLES,
137
- centerIndex + NEAREST_HINT_WINDOW_SAMPLES,
138
- );
139
- const fallbackDistance = track.width / 2 + (track.kerbWidth ?? 0) + track.gravelWidth + track.runoffWidth + 180;
140
- if (!nearest.best || nearest.bestDistance > fallbackDistance * fallbackDistance) {
141
- nearest = null;
142
- }
143
- }
144
-
145
- return nearest?.best ?? nearestSampleGlobal(track, position).best;
146
- }
147
-
148
125
  export function offsetTrackPoint(point, offset) {
149
126
  return {
150
127
  x: point.x + point.normalX * offset,