@inventure71/paddockjs 3.0.0 → 4.1.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 (97) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/INSTALL_AND_UPDATE.md +21 -4
  3. package/README.md +103 -551
  4. package/docs/architecture.md +28 -18
  5. package/docs/component_inventory.md +5 -4
  6. package/docs/composable-layouts.md +79 -0
  7. package/docs/custom_model_controller.md +4 -4
  8. package/docs/data-helpers.md +50 -0
  9. package/docs/data_contract.md +13 -7
  10. package/docs/getting-started.md +105 -0
  11. package/docs/headless-environment.md +68 -0
  12. package/docs/index.md +17 -4
  13. package/docs/rules.md +6 -2
  14. package/docs/system_specs.md +60 -7
  15. package/docs/theming.md +64 -0
  16. package/docs/training.md +20 -14
  17. package/docs/troubleshooting.md +76 -0
  18. package/docs/upgrading-hosts-to-3.md +42 -0
  19. package/examples/python/README.md +6 -0
  20. package/examples/python/base_policy_server.py +19 -0
  21. package/package.json +21 -5
  22. package/src/api/PaddockSimulatorController.js +78 -22
  23. package/src/api/runtimeTheme.js +59 -0
  24. package/src/app/F1SimulatorApp.js +24 -3
  25. package/src/app/readouts/timingTowerRenderer.js +203 -42
  26. package/src/app/runtime/frameLoop.js +5 -1
  27. package/src/config/defaultOptions.js +8 -1
  28. package/src/config/themeOptions.js +5 -0
  29. package/src/data/index.d.ts +212 -0
  30. package/src/data/index.js +14 -0
  31. package/src/environment/episode.js +11 -1
  32. package/src/environment/metrics.js +59 -16
  33. package/src/environment/observationVector.js +50 -33
  34. package/src/environment/observations.js +89 -20
  35. package/src/environment/runtime.js +41 -0
  36. package/src/environment/sensors/carRays.js +48 -37
  37. package/src/environment/sensors/index.js +501 -97
  38. package/src/environment/sensors/indexedRayBands.js +134 -46
  39. package/src/environment/sensors/rayBandTrace.js +1171 -0
  40. package/src/environment/sensors/rayChannels.js +108 -0
  41. package/src/environment/sensors/rayConfig.js +71 -25
  42. package/src/environment/sensors/rayGeometry.js +23 -17
  43. package/src/environment/sensors/sensorTargets.js +67 -1
  44. package/src/environment/sensors/surfaceRays.js +130 -89
  45. package/src/environment/sensors/trackRays.js +115 -1
  46. package/src/index.d.ts +43 -0
  47. package/src/index.js +27 -2
  48. package/src/placeholder/index.d.ts +11 -0
  49. package/src/placeholder/index.js +61 -0
  50. package/src/placeholder.css +72 -0
  51. package/src/simulation/collisionGeometry.js +260 -96
  52. package/src/simulation/pit/pitFlow.js +21 -9
  53. package/src/simulation/pit/pitQueue.js +5 -3
  54. package/src/simulation/pit/pitRouteMovement.js +9 -4
  55. package/src/simulation/pit/pitRouting.js +77 -42
  56. package/src/simulation/pit/pitState.js +1 -0
  57. package/src/simulation/pit/pitTireService.js +7 -3
  58. package/src/simulation/race/raceFinish.js +25 -8
  59. package/src/simulation/race/raceOrder.js +189 -32
  60. package/src/simulation/race/raceProgress.js +129 -37
  61. package/src/simulation/race/raceSetup.js +2 -0
  62. package/src/simulation/race/raceStep.js +25 -13
  63. package/src/simulation/raceSimulation.js +20 -17
  64. package/src/simulation/rules/collisionSteward.js +40 -23
  65. package/src/simulation/rules/rulesReview.js +77 -56
  66. package/src/simulation/snapshots/raceSnapshots.js +37 -0
  67. package/src/simulation/timing/drsTiming.js +143 -30
  68. package/src/simulation/timing/gapEstimation.js +25 -3
  69. package/src/simulation/timing/lapTelemetry.js +115 -36
  70. package/src/simulation/timing/raceTiming.js +5 -1
  71. package/src/simulation/timing/sectorPerformance.js +104 -26
  72. package/src/simulation/timing/timingHistory.js +229 -14
  73. package/src/simulation/timing/timingLines.js +65 -7
  74. package/src/simulation/track/pitLaneLayout.js +23 -0
  75. package/src/simulation/track/pitLaneState.js +218 -19
  76. package/src/simulation/track/spatialQueries.js +67 -11
  77. package/src/simulation/track/trackMath.js +91 -24
  78. package/src/simulation/track/trackModel.js +2 -0
  79. package/src/simulation/track/trackQueryIndex.js +1334 -100
  80. package/src/simulation/track/trackQueryPitIndex.js +138 -14
  81. package/src/simulation/track/trackQueryScratch.js +7 -0
  82. package/src/simulation/track/trackQueryStats.js +25 -0
  83. package/src/simulation/track/trackSnapshotJson.js +139 -0
  84. package/src/simulation/track/trackStatePolicy.js +249 -1
  85. package/src/simulation/vehicle/contactResolution.js +96 -59
  86. package/src/simulation/vehicle/mainTrackWheelSurface.js +73 -34
  87. package/src/simulation/vehicle/pitWheelSurface.js +81 -28
  88. package/src/simulation/vehicle/runoffResponse.js +54 -29
  89. package/src/simulation/vehicle/vehicleGeometry.js +245 -51
  90. package/src/simulation/vehicle/vehiclePhysics.js +42 -27
  91. package/src/simulation/vehicle/vehicleSnapshots.js +23 -0
  92. package/src/simulation/vehicle/vehicleState.js +6 -0
  93. package/src/simulation/vehicle/wheelSurface.js +807 -92
  94. package/src/styles.css +497 -497
  95. package/src/ui/raceControlsTemplate.js +1 -0
  96. package/src/ui/telemetryDrawerTemplate.js +1 -0
  97. package/src/ui/telemetryTemplates.js +1 -0
@@ -1,4 +1,9 @@
1
- import { buildCollisionCandidatePairs, detectVehicleCollision } from '../collisionGeometry.js';
1
+ import {
2
+ buildCollisionCandidatePairs,
3
+ collisionCandidatePairFirstIndex,
4
+ collisionCandidatePairSecondIndex,
5
+ detectVehicleCollision,
6
+ } from '../collisionGeometry.js';
2
7
  import { canCollide, isCollidable } from '../participants/participantInteractions.js';
3
8
  import { clamp, normalizeAngle } from '../simMath.js';
4
9
  import { VEHICLE_LIMITS } from './vehiclePhysics.js';
@@ -11,54 +16,84 @@ function isPitPositionControlledCar(car) {
11
16
  return status === 'entering' || status === 'queued' || status === 'servicing' || status === 'exiting';
12
17
  }
13
18
 
14
- function forwardVector(car) {
15
- return { x: Math.cos(car.heading), y: Math.sin(car.heading) };
19
+ function velocityComponentX(car) {
20
+ return Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)
21
+ ? car.velocityX
22
+ : Math.cos(car.heading) * car.speed;
16
23
  }
17
24
 
18
- function velocityVector(car) {
19
- if (Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)) {
20
- return { x: car.velocityX, y: car.velocityY };
21
- }
22
- const forward = forwardVector(car);
23
- return { x: forward.x * car.speed, y: forward.y * car.speed };
24
- }
25
-
26
- function dot(a, b) {
27
- return a.x * b.x + a.y * b.y;
25
+ function velocityComponentY(car) {
26
+ return Number.isFinite(car.velocityX) && Number.isFinite(car.velocityY)
27
+ ? car.velocityY
28
+ : Math.sin(car.heading) * car.speed;
28
29
  }
29
30
 
30
- function applyVelocityVector(car, velocity) {
31
- const speed = clamp(Math.hypot(velocity.x, velocity.y), 0, VEHICLE_LIMITS.maxSpeed);
31
+ function applyVelocityScalars(car, velocityX, velocityY) {
32
+ const rawSpeed = Math.hypot(velocityX, velocityY);
33
+ const speed = clamp(rawSpeed, 0, VEHICLE_LIMITS.maxSpeed);
32
34
  if (speed <= 0) {
33
35
  car.velocityX = 0;
34
36
  car.velocityY = 0;
35
37
  car.speed = 0;
36
38
  return;
37
39
  }
38
- const scale = speed / Math.max(1e-9, Math.hypot(velocity.x, velocity.y));
39
- car.velocityX = velocity.x * scale;
40
- car.velocityY = velocity.y * scale;
40
+ const scale = speed / Math.max(1e-9, rawSpeed);
41
+ car.velocityX = velocityX * scale;
42
+ car.velocityY = velocityY * scale;
41
43
  car.speed = speed;
42
44
  }
43
45
 
46
+ function recordContactVelocityResponse(stats) {
47
+ if (!stats) return;
48
+ stats.contactVelocityResponses += 1;
49
+ stats.contactVelocityVectorObjectAllocations += 0;
50
+ }
51
+
52
+ function prepareReportedContactMarks(scratch, carCount) {
53
+ const requiredLength = Math.max(1, carCount * carCount);
54
+ if (!(scratch.reportedContactMarks instanceof Uint32Array) || scratch.reportedContactMarks.length < requiredLength) {
55
+ scratch.reportedContactMarks = new Uint32Array(requiredLength);
56
+ }
57
+ scratch.reportedContactEpoch = ((scratch.reportedContactEpoch ?? 0) + 1) >>> 0;
58
+ if (scratch.reportedContactEpoch === 0) {
59
+ scratch.reportedContactMarks.fill(0);
60
+ scratch.reportedContactEpoch = 1;
61
+ }
62
+ return scratch.reportedContactMarks;
63
+ }
64
+
65
+ function contactMarkIndex(firstIndex, secondIndex, carCount) {
66
+ const low = firstIndex < secondIndex ? firstIndex : secondIndex;
67
+ const high = firstIndex < secondIndex ? secondIndex : firstIndex;
68
+ return low * carCount + high;
69
+ }
70
+
44
71
  export function resolveCollisionsForSimulation(sim) {
45
- const collidableCars = sim.cars.filter(isCollidable);
72
+ sim.collisionScratch ??= {};
73
+ const collidableCars = sim.collisionScratch.collidableCars ?? [];
74
+ sim.collisionScratch.collidableCars = collidableCars;
75
+ collidableCars.length = 0;
76
+ for (let index = 0; index < sim.cars.length; index += 1) {
77
+ const car = sim.cars[index];
78
+ if (isCollidable(car)) collidableCars.push(car);
79
+ }
46
80
  if (collidableCars.length < 2) return;
47
- const reportedContacts = new Set();
81
+ const reportedContactMarks = prepareReportedContactMarks(sim.collisionScratch, collidableCars.length);
82
+ const reportedContactEpoch = sim.collisionScratch.reportedContactEpoch;
48
83
 
49
84
  for (let pass = 0; pass < 3; pass += 1) {
50
- const candidates = buildCollisionCandidatePairs(collidableCars, { trackLength: sim.track.length });
51
- for (const [first, second] of candidates) {
85
+ const candidates = buildCollisionCandidatePairs(collidableCars, {
86
+ trackLength: sim.track.length,
87
+ scratch: sim.collisionScratch,
88
+ });
89
+ for (const pair of candidates) {
90
+ const [first, second] = pair;
52
91
  if (!canCollide(first, second)) continue;
53
- const collision = detectVehicleCollision(first, second);
92
+ const collision = detectVehicleCollision(first, second, { scratch: sim.collisionScratch });
54
93
  if (!collision) continue;
55
94
  const firstPitControlled = isPitPositionControlledCar(first);
56
95
  const secondPitControlled = isPitPositionControlledCar(second);
57
96
  if (firstPitControlled && secondPitControlled) continue;
58
- const stewardCollision = {
59
- ...collision,
60
- trackLength: sim.track.length,
61
- };
62
97
  const oneCarFixed = firstPitControlled || secondPitControlled;
63
98
 
64
99
  const correction = Math.min(
@@ -100,9 +135,15 @@ export function resolveCollisionsForSimulation(sim) {
100
135
  first.contactCooldown = 1;
101
136
  second.contactCooldown = 1;
102
137
 
103
- const contactKey = `${first.id}:${second.id}`;
104
- if (freshContact && pass === 0 && !reportedContacts.has(contactKey)) {
105
- reportedContacts.add(contactKey);
138
+ const firstPairIndex = collisionCandidatePairFirstIndex(pair);
139
+ const secondPairIndex = collisionCandidatePairSecondIndex(pair);
140
+ const canMarkContact = firstPairIndex >= 0 && secondPairIndex >= 0;
141
+ const contactIndex = canMarkContact
142
+ ? contactMarkIndex(firstPairIndex, secondPairIndex, collidableCars.length)
143
+ : -1;
144
+ const alreadyReported = canMarkContact && reportedContactMarks[contactIndex] === reportedContactEpoch;
145
+ if (freshContact && pass === 0 && !alreadyReported) {
146
+ if (canMarkContact) reportedContactMarks[contactIndex] = reportedContactEpoch;
106
147
  sim.events.unshift({
107
148
  type: 'contact',
108
149
  at: sim.time,
@@ -114,48 +155,44 @@ export function resolveCollisionsForSimulation(sim) {
114
155
  depth: collision.depth,
115
156
  timeOfImpact: collision.timeOfImpact,
116
157
  });
117
- sim.reviewCollision(first, second, stewardCollision);
158
+ sim.reviewCollision(first, second, collision, {
159
+ scratch: sim.collisionScratch,
160
+ trackLength: sim.track.length,
161
+ });
118
162
  }
119
163
  }
120
164
  }
121
165
  }
122
166
 
123
- export function applyContactVelocityResponse(sim, first, second, axis) {
167
+ export function applyContactVelocityResponse(sim, first, second, axis, options = {}) {
168
+ recordContactVelocityResponse(options.stats);
124
169
  if (
125
170
  sim.physicsMode === 'advanced' ||
126
171
  Number.isFinite(first.velocityX) ||
127
172
  Number.isFinite(second.velocityX)
128
173
  ) {
129
- const firstVelocity = velocityVector(first);
130
- const secondVelocity = velocityVector(second);
131
- const relativeNormalVelocity = dot({
132
- x: secondVelocity.x - firstVelocity.x,
133
- y: secondVelocity.y - firstVelocity.y,
134
- }, axis);
174
+ let firstVelocityX = velocityComponentX(first);
175
+ let firstVelocityY = velocityComponentY(first);
176
+ let secondVelocityX = velocityComponentX(second);
177
+ let secondVelocityY = velocityComponentY(second);
178
+ const relativeNormalVelocity = (secondVelocityX - firstVelocityX) * axis.x +
179
+ (secondVelocityY - firstVelocityY) * axis.y;
135
180
 
136
181
  if (relativeNormalVelocity < 0) {
137
182
  const impulse = clamp(-relativeNormalVelocity * (0.34 + sim.rules.collisionRestitution), 0, 16);
138
- firstVelocity.x -= axis.x * impulse;
139
- firstVelocity.y -= axis.y * impulse;
140
- secondVelocity.x += axis.x * impulse;
141
- secondVelocity.y += axis.y * impulse;
183
+ firstVelocityX -= axis.x * impulse;
184
+ firstVelocityY -= axis.y * impulse;
185
+ secondVelocityX += axis.x * impulse;
186
+ secondVelocityY += axis.y * impulse;
142
187
  }
143
188
 
144
- applyVelocityVector(first, {
145
- x: firstVelocity.x * 0.997,
146
- y: firstVelocity.y * 0.997,
147
- });
148
- applyVelocityVector(second, {
149
- x: secondVelocity.x * 0.997,
150
- y: secondVelocity.y * 0.997,
151
- });
189
+ applyVelocityScalars(first, firstVelocityX * 0.997, firstVelocityY * 0.997);
190
+ applyVelocityScalars(second, secondVelocityX * 0.997, secondVelocityY * 0.997);
152
191
  return;
153
192
  }
154
193
 
155
- const firstForward = forwardVector(first);
156
- const secondForward = forwardVector(second);
157
- const firstNormal = dot(firstForward, axis);
158
- const secondNormal = dot(secondForward, axis);
194
+ const firstNormal = Math.cos(first.heading) * axis.x + Math.sin(first.heading) * axis.y;
195
+ const secondNormal = Math.cos(second.heading) * axis.x + Math.sin(second.heading) * axis.y;
159
196
  const relativeNormalVelocity = second.speed * secondNormal - first.speed * firstNormal;
160
197
 
161
198
  if (relativeNormalVelocity < 0) {
@@ -170,11 +207,11 @@ export function applyContactVelocityResponse(sim, first, second, axis) {
170
207
 
171
208
  function dampPitContactVelocity(car, factor, syncVelocity = false) {
172
209
  if (syncVelocity || Number.isFinite(car.velocityX) || Number.isFinite(car.velocityY)) {
173
- const velocity = velocityVector(car);
174
- applyVelocityVector(car, {
175
- x: velocity.x * factor,
176
- y: velocity.y * factor,
177
- });
210
+ applyVelocityScalars(
211
+ car,
212
+ velocityComponentX(car) * factor,
213
+ velocityComponentY(car) * factor,
214
+ );
178
215
  return;
179
216
  }
180
217
  car.speed = clamp(car.speed * factor, 0, VEHICLE_LIMITS.maxSpeed);
@@ -11,8 +11,13 @@ export function mainTrackSurface(track, crossTrackError) {
11
11
  }
12
12
 
13
13
  export function wheelFullyOutside(samples, trackLimit) {
14
- const rightOutside = samples.every((state) => !state.inPitLane && state.signedOffset > trackLimit);
15
- const leftOutside = samples.every((state) => !state.inPitLane && state.signedOffset < -trackLimit);
14
+ let rightOutside = true;
15
+ let leftOutside = true;
16
+ for (let index = 0; index < samples.length; index += 1) {
17
+ const state = samples[index];
18
+ if (state.inPitLane || state.signedOffset <= trackLimit) rightOutside = false;
19
+ if (state.inPitLane || state.signedOffset >= -trackLimit) leftOutside = false;
20
+ }
16
21
  return {
17
22
  rightOutside,
18
23
  leftOutside,
@@ -33,50 +38,84 @@ export function wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset
33
38
  }
34
39
 
35
40
  export function analyticWheelState(patch, centerState, track, trackLimit) {
41
+ return writeAnalyticWheelStateWithProjectedHalfWidth(
42
+ {},
43
+ patch,
44
+ centerState,
45
+ track,
46
+ trackLimit,
47
+ analyticWheelProjectedHalfWidth(patch, centerState),
48
+ );
49
+ }
50
+
51
+ export function writeAnalyticWheelState(target, patch, centerState, track, trackLimit) {
52
+ return writeAnalyticWheelStateWithProjectedHalfWidth(
53
+ target,
54
+ patch,
55
+ centerState,
56
+ track,
57
+ trackLimit,
58
+ analyticWheelProjectedHalfWidth(patch, centerState),
59
+ );
60
+ }
61
+
62
+ export function analyticWheelProjectedHalfWidth(patch, centerState) {
63
+ return (
64
+ Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
65
+ Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth
66
+ );
67
+ }
68
+
69
+ export function writeAnalyticWheelStateWithProjectedHalfWidth(
70
+ target,
71
+ patch,
72
+ centerState,
73
+ track,
74
+ trackLimit,
75
+ projectedHalfWidth,
76
+ ) {
36
77
  const wheelCenterOffset =
37
78
  (patch.center.x - centerState.x) * centerState.normalX +
38
79
  (patch.center.y - centerState.y) * centerState.normalY;
39
- const projectedHalfWidth =
40
- Math.abs(patch.forward.x * centerState.normalX + patch.forward.y * centerState.normalY) * patch.halfLength +
41
- Math.abs(patch.right.x * centerState.normalX + patch.right.y * centerState.normalY) * patch.halfWidth;
42
80
  const minimumSignedOffset = wheelCenterOffset - projectedHalfWidth;
43
81
  const maximumSignedOffset = wheelCenterOffset + projectedHalfWidth;
44
82
  const signedOffset = Math.abs(minimumSignedOffset) > Math.abs(maximumSignedOffset)
45
83
  ? minimumSignedOffset
46
84
  : maximumSignedOffset;
47
- const state = stateFromSignedOffset(centerState, track, signedOffset);
48
85
  const outside = wheelOutsideFromOffsets(minimumSignedOffset, maximumSignedOffset, trackLimit);
86
+ const sampledStates = target.sampledStates ?? [];
87
+ const state = sampledStates.length === 1 ? (sampledStates[0] ?? {}) : {};
88
+ sampledStates.length = 1;
89
+ sampledStates[0] = state;
90
+ writeStateFromSignedOffset(state, centerState, track, signedOffset);
49
91
 
50
- return {
51
- id: patch.id,
52
- x: patch.center.x,
53
- y: patch.center.y,
54
- signedOffset: state.signedOffset,
55
- crossTrackError: state.crossTrackError,
56
- surface: state.surface,
57
- onTrack: Boolean(state.onTrack),
58
- inPitLane: false,
59
- pitLanePart: null,
60
- pitBoxId: null,
61
- minimumSignedOffset,
62
- maximumSignedOffset,
63
- fullyOutsideWhiteLine: outside.fullyOutsideWhiteLine,
64
- outsideSide: outside.outsideSide,
65
- sampledStates: [state],
66
- };
92
+ target.id = patch.id;
93
+ target.x = patch.center.x;
94
+ target.y = patch.center.y;
95
+ target.signedOffset = state.signedOffset;
96
+ target.crossTrackError = state.crossTrackError;
97
+ target.surface = state.surface;
98
+ target.onTrack = Boolean(state.onTrack);
99
+ target.inPitLane = false;
100
+ target.pitLanePart = null;
101
+ target.pitBoxId = null;
102
+ target.minimumSignedOffset = minimumSignedOffset;
103
+ target.maximumSignedOffset = maximumSignedOffset;
104
+ target.fullyOutsideWhiteLine = outside.fullyOutsideWhiteLine;
105
+ target.outsideSide = outside.outsideSide;
106
+ target.sampledStates = sampledStates;
107
+ return target;
67
108
  }
68
109
 
69
- function stateFromSignedOffset(centerState, track, signedOffset) {
110
+ function writeStateFromSignedOffset(target, centerState, track, signedOffset) {
70
111
  const crossTrackError = Math.abs(signedOffset);
71
112
  const surface = mainTrackSurface(track, crossTrackError);
72
- return {
73
- ...centerState,
74
- signedOffset,
75
- crossTrackError,
76
- surface,
77
- onTrack: surface === 'track' || surface === 'kerb',
78
- inPitLane: false,
79
- pitLanePart: null,
80
- pitBoxId: null,
81
- };
113
+ target.signedOffset = signedOffset;
114
+ target.crossTrackError = crossTrackError;
115
+ target.surface = surface;
116
+ target.onTrack = surface === 'track' || surface === 'kerb';
117
+ target.inPitLane = false;
118
+ target.pitLanePart = null;
119
+ target.pitBoxId = null;
120
+ return target;
82
121
  }
@@ -2,10 +2,6 @@ import { metersToSimUnits } from '../units.js';
2
2
 
3
3
  const PIT_CONNECTOR_FULL_SAMPLE_WINDOW = metersToSimUnits(35);
4
4
 
5
- export function patchSamples(patch) {
6
- return [patch.center, ...patch.corners];
7
- }
8
-
9
5
  export function isNearPitConnector(track, centerState) {
10
6
  const pitLane = track.pitLane;
11
7
  if (!pitLane?.enabled) return false;
@@ -20,33 +16,81 @@ export function canUseAnalyticPitWheels(geometry, centerState) {
20
16
  const roadWidth = Number(centerState?.pitLaneRoadWidth);
21
17
  if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
22
18
  const halfRoadWidth = roadWidth / 2;
23
- return geometry.contactPatches.every((patch) => {
24
- const range = patchPitOffsetRange(patch, centerState);
25
- return range.minimum >= -halfRoadWidth - 0.001 && range.maximum <= halfRoadWidth + 0.001;
26
- });
19
+ for (let index = 0; index < geometry.contactPatches.length; index += 1) {
20
+ const range = patchPitOffsetRange(geometry.contactPatches[index], centerState);
21
+ if (range.minimum < -halfRoadWidth - 0.001 || range.maximum > halfRoadWidth + 0.001) return false;
22
+ }
23
+ return true;
24
+ }
25
+
26
+ export function canSkipConnectorPitChecks(track, geometry, centerState, epsilon = 0.001) {
27
+ const laneSide = Math.sign(track?.pitLane?.side ?? 0);
28
+ if (
29
+ !laneSide ||
30
+ !geometry?.contactPatches?.length ||
31
+ !centerState ||
32
+ centerState.inPitLane ||
33
+ !centerState.onTrack
34
+ ) return false;
35
+ for (let index = 0; index < geometry.contactPatches.length; index += 1) {
36
+ if (!connectorPitCheckCanBeSkippedForPatch(track, geometry.contactPatches[index], centerState, epsilon)) return false;
37
+ }
38
+ return true;
39
+ }
40
+
41
+ export function pitPatchOffsetRange(patch, centerState) {
42
+ return patchPitOffsetRange(patch, centerState);
27
43
  }
28
44
 
29
- export function analyticPitWheelState(patch, centerState) {
45
+ export function pitPatchInsideRoad(patch, centerState, epsilon = 0.001) {
46
+ const roadWidth = Number(centerState?.pitLaneRoadWidth);
47
+ if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
48
+ const halfRoadWidth = roadWidth / 2;
30
49
  const range = patchPitOffsetRange(patch, centerState);
31
- return {
32
- id: patch.id,
33
- x: patch.center.x,
34
- y: patch.center.y,
35
- signedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
36
- crossTrackError: centerState.mainTrackCrossTrackError ?? centerState.crossTrackError,
37
- surface: centerState.surface,
38
- onTrack: true,
39
- inPitLane: true,
40
- pitLanePart: centerState.pitLanePart ?? null,
41
- pitBoxId: centerState.pitBoxId ?? null,
42
- minimumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
43
- maximumSignedOffset: centerState.mainTrackSignedOffset ?? centerState.signedOffset,
44
- pitLaneMinimumSignedOffset: range.minimum,
45
- pitLaneMaximumSignedOffset: range.maximum,
46
- fullyOutsideWhiteLine: false,
47
- outsideSide: 0,
48
- sampledStates: [centerState],
49
- };
50
+ return range.minimum >= -halfRoadWidth - epsilon && range.maximum <= halfRoadWidth + epsilon;
51
+ }
52
+
53
+ export function pitPatchOverlapsRoad(patch, centerState, epsilon = 0.001) {
54
+ const roadWidth = Number(centerState?.pitLaneRoadWidth);
55
+ if (!centerState?.inPitLane || !Number.isFinite(roadWidth) || roadWidth <= 0) return false;
56
+ const halfRoadWidth = roadWidth / 2;
57
+ const range = patchPitOffsetRange(patch, centerState);
58
+ return range.minimum <= halfRoadWidth + epsilon && range.maximum >= -halfRoadWidth - epsilon;
59
+ }
60
+
61
+ export function writeAnalyticPitWheelState(target, patch, centerState) {
62
+ const range = patchPitOffsetRange(patch, centerState);
63
+ const signedOffset = centerState.mainTrackSignedOffset ?? centerState.signedOffset;
64
+ const sampledStates = target.sampledStates ?? [];
65
+ const sampleState = sampledStates.length === 1 ? (sampledStates[0] ?? {}) : {};
66
+ sampledStates.length = 1;
67
+ sampledStates[0] = sampleState;
68
+ sampleState.signedOffset = centerState.signedOffset;
69
+ sampleState.crossTrackError = centerState.crossTrackError;
70
+ sampleState.surface = centerState.surface;
71
+ sampleState.onTrack = Boolean(centerState.onTrack);
72
+ sampleState.inPitLane = true;
73
+ sampleState.pitLanePart = centerState.pitLanePart ?? null;
74
+ sampleState.pitBoxId = centerState.pitBoxId ?? null;
75
+
76
+ target.id = patch.id;
77
+ target.x = patch.center.x;
78
+ target.y = patch.center.y;
79
+ target.signedOffset = signedOffset;
80
+ target.crossTrackError = centerState.mainTrackCrossTrackError ?? centerState.crossTrackError;
81
+ target.surface = centerState.surface;
82
+ target.onTrack = true;
83
+ target.inPitLane = true;
84
+ target.pitLanePart = centerState.pitLanePart ?? null;
85
+ target.pitBoxId = centerState.pitBoxId ?? null;
86
+ target.minimumSignedOffset = signedOffset;
87
+ target.maximumSignedOffset = signedOffset;
88
+ target.pitLaneMinimumSignedOffset = range.minimum;
89
+ target.pitLaneMaximumSignedOffset = range.maximum;
90
+ target.fullyOutsideWhiteLine = false;
91
+ target.outsideSide = 0;
92
+ target.sampledStates = sampledStates;
93
+ return target;
50
94
  }
51
95
 
52
96
  function patchPitOffsetRange(patch, centerState) {
@@ -62,6 +106,15 @@ function patchPitOffsetRange(patch, centerState) {
62
106
  };
63
107
  }
64
108
 
109
+ function connectorPitCheckCanBeSkippedForPatch(track, patch, centerState, epsilon) {
110
+ const laneSide = Math.sign(track?.pitLane?.side ?? 0);
111
+ if (!laneSide || !patch || !centerState || centerState.inPitLane || !centerState.onTrack) return false;
112
+ const range = patchPitOffsetRange(patch, centerState);
113
+ return laneSide > 0
114
+ ? range.maximum <= epsilon
115
+ : range.minimum >= -epsilon;
116
+ }
117
+
65
118
  function wrappedDistanceDelta(first, second, length) {
66
119
  if (!Number.isFinite(first) || !Number.isFinite(second) || !Number.isFinite(length) || length <= 0) return Infinity;
67
120
  const delta = Math.abs(first - second);
@@ -1,58 +1,83 @@
1
- import { VEHICLE_GEOMETRY, getVehicleGeometryState, vehicleAxes } from './vehicleGeometry.js';
1
+ import {
2
+ VEHICLE_GEOMETRY,
3
+ vehicleAxes,
4
+ } from './vehicleGeometry.js';
2
5
  import { applyWheelSurfaceState } from './wheelSurface.js';
3
6
  import { freezeVehicleMotion } from './vehicleKinematics.js';
4
- import { nearestTrackStateForCar, pitOverrideAllowedForCar } from '../track/trackStatePolicy.js';
7
+ import { queryRunoffTrackStateForCar } from '../track/trackStatePolicy.js';
5
8
  import { markCarDnf } from '../race/retirements.js';
6
9
 
10
+ export function clearRunoffCenterState(car) {
11
+ if (car) car.pendingRunoffCenterState = null;
12
+ }
13
+
14
+ export function storeRunoffCenterState(car, state) {
15
+ car.pendingRunoffCenterState = {
16
+ state,
17
+ x: car.x,
18
+ y: car.y,
19
+ heading: car.heading,
20
+ };
21
+ }
22
+
23
+ export function takeValidRunoffCenterState(car) {
24
+ const pending = car?.pendingRunoffCenterState;
25
+ if (!pending) return null;
26
+ car.pendingRunoffCenterState = null;
27
+ return pending.x === car.x &&
28
+ pending.y === car.y &&
29
+ pending.heading === car.heading
30
+ ? pending.state
31
+ : null;
32
+ }
33
+
7
34
  export function applyRunoffResponseForSimulation(sim, car) {
8
35
  if (car.destroyed) {
36
+ clearRunoffCenterState(car);
9
37
  freezeBarrierMotion(car, sim.physicsMode);
10
38
  applyWheelSurfaceState(car, sim.track);
11
39
  return;
12
40
  }
13
- const allowPitOverride = pitOverrideAllowedForCar(car);
14
- const state = nearestTrackStateForCar(sim.track, car, car, car.progress, { allowPitOverride });
15
- const mainTrackState = state.inPitLane
16
- ? nearestTrackStateForCar(sim.track, car, car, car.progress, { allowPitOverride: false })
17
- : state;
41
+ const { state, mainTrackState } = queryRunoffTrackStateForCar(sim.track, car);
18
42
  const barrierCenter = sim.track.width / 2 + (sim.track.kerbWidth ?? 0) + sim.track.gravelWidth + sim.track.runoffWidth;
19
43
  const signedLimit = barrierCenter - (sim.track.barrierWidth ?? 0) / 2;
20
44
  const side = Math.sign(mainTrackState.signedOffset) || 1;
21
45
  const outwardReach = getOutwardVehicleReach(car, mainTrackState, side);
22
46
  const overshoot = Math.abs(mainTrackState.signedOffset) + outwardReach - signedLimit;
23
47
  if (overshoot <= 0) {
24
- if (state.inPitLane) {
25
- applyWheelSurfaceState(car, sim.track, { centerState: state });
26
- return;
27
- }
28
- applyWheelSurfaceState(car, sim.track, { centerState: state });
48
+ storeRunoffCenterState(car, state);
29
49
  return;
30
50
  }
31
51
 
52
+ clearRunoffCenterState(car);
32
53
  destroyCarOnBarrier(sim, car);
33
54
  applyWheelSurfaceState(car, sim.track, { centerState: mainTrackState });
34
55
  }
35
56
 
36
57
  function getOutwardVehicleReach(car, state, side) {
37
- const visualReach = getVisualFootprintOutwardReach(car, state);
38
- const geometry = getVehicleGeometryState(car);
39
- let reach = visualReach;
40
- geometry.current.shapes.forEach((shape) => {
41
- shape.corners.forEach((corner) => {
42
- const lateral =
43
- (corner.x - car.x) * state.normalX +
44
- (corner.y - car.y) * state.normalY;
45
- reach = Math.max(reach, lateral * side);
46
- });
47
- });
48
- return reach;
58
+ const axes = vehicleAxes(car.heading ?? 0);
59
+ const forwardDot = axes.forward.x * state.normalX + axes.forward.y * state.normalY;
60
+ const rightDot = axes.right.x * state.normalX + axes.right.y * state.normalY;
61
+ return Math.max(
62
+ orientedHalfExtent(VEHICLE_GEOMETRY.visualLength / 2, VEHICLE_GEOMETRY.visualWidth / 2, forwardDot, rightDot),
63
+ orientedHalfExtent(VEHICLE_GEOMETRY.bodyLength / 2, VEHICLE_GEOMETRY.bodyWidth / 2, forwardDot, rightDot),
64
+ wheelOutwardReach(forwardDot, rightDot, side),
65
+ );
49
66
  }
50
67
 
51
- function getVisualFootprintOutwardReach(car, state) {
52
- const axes = vehicleAxes(car.heading ?? 0);
53
- return (
54
- Math.abs(axes.forward.x * state.normalX + axes.forward.y * state.normalY) * VEHICLE_GEOMETRY.visualLength / 2 +
55
- Math.abs(axes.right.x * state.normalX + axes.right.y * state.normalY) * VEHICLE_GEOMETRY.visualWidth / 2
68
+ function orientedHalfExtent(halfLength, halfWidth, forwardDot, rightDot) {
69
+ return Math.abs(forwardDot) * halfLength + Math.abs(rightDot) * halfWidth;
70
+ }
71
+
72
+ function wheelOutwardReach(forwardDot, rightDot, side) {
73
+ const centerOffset =
74
+ Math.abs(forwardDot) * VEHICLE_GEOMETRY.wheelLongitudinalOffset +
75
+ Math.abs(rightDot * side) * VEHICLE_GEOMETRY.wheelLateralOffset;
76
+ return centerOffset + orientedHalfExtent(
77
+ VEHICLE_GEOMETRY.wheelLength / 2,
78
+ VEHICLE_GEOMETRY.wheelWidth / 2,
79
+ forwardDot,
80
+ rightDot,
56
81
  );
57
82
  }
58
83