@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
@@ -0,0 +1,61 @@
1
+ function escapeHtml(value) {
2
+ return String(value ?? '')
3
+ .replaceAll('&', '&')
4
+ .replaceAll('<', '&lt;')
5
+ .replaceAll('>', '&gt;')
6
+ .replaceAll('"', '&quot;')
7
+ .replaceAll("'", '&#39;');
8
+ }
9
+
10
+ function normalizeVariant(value) {
11
+ return value === 'custom' ? 'custom' : 'lights';
12
+ }
13
+
14
+ function renderAttributes(attributes = {}) {
15
+ if (!attributes || typeof attributes !== 'object') return '';
16
+ return Object.entries(attributes)
17
+ .filter(([name, value]) => (
18
+ value != null &&
19
+ value !== false &&
20
+ /^[a-zA-Z_][\w:.-]*$/.test(name) &&
21
+ !/^on/i.test(name)
22
+ ))
23
+ .map(([name, value]) => (value === true
24
+ ? ` ${escapeHtml(name)}`
25
+ : ` ${escapeHtml(name)}="${escapeHtml(value)}"`))
26
+ .join('');
27
+ }
28
+
29
+ export function createPaddockLoadingPlaceholder({
30
+ label = 'Loading simulator',
31
+ detail = '',
32
+ className = '',
33
+ variant = 'lights',
34
+ attributes,
35
+ } = {}) {
36
+ const normalizedVariant = normalizeVariant(variant);
37
+ const extraClassName = className ? ` ${escapeHtml(className)}` : '';
38
+ const detailMarkup = detail
39
+ ? `<span class="paddock-placeholder__detail">${escapeHtml(detail)}</span>`
40
+ : '';
41
+ const lightsMarkup = normalizedVariant === 'lights'
42
+ ? `
43
+ <div class="paddock-placeholder__lights" aria-hidden="true">
44
+ <span></span>
45
+ <span></span>
46
+ <span></span>
47
+ <span></span>
48
+ <span></span>
49
+ </div>`
50
+ : '';
51
+
52
+ return `
53
+ <div class="paddock-placeholder${extraClassName}" data-paddock-placeholder data-paddock-placeholder-variant="${normalizedVariant}" role="status" aria-live="polite" aria-busy="true"${renderAttributes(attributes)}>
54
+ <div class="paddock-placeholder__content">
55
+ ${lightsMarkup}
56
+ <span class="paddock-placeholder__label">${escapeHtml(label)}</span>
57
+ ${detailMarkup}
58
+ </div>
59
+ </div>
60
+ `;
61
+ }
@@ -0,0 +1,72 @@
1
+ .paddock-placeholder {
2
+ --paddock-placeholder-background: #000;
3
+ --paddock-placeholder-color: rgba(244, 241, 234, 0.78);
4
+ --paddock-placeholder-light: #ff1f18;
5
+ --paddock-placeholder-light-dim: #8f0804;
6
+ --paddock-placeholder-min-height: 420px;
7
+ position: relative;
8
+ display: grid;
9
+ min-height: var(--paddock-placeholder-min-height);
10
+ place-items: center;
11
+ overflow: hidden;
12
+ color: var(--paddock-placeholder-color);
13
+ background: var(--paddock-placeholder-background);
14
+ }
15
+
16
+ .paddock-placeholder__content {
17
+ display: grid;
18
+ justify-items: center;
19
+ gap: 0.8rem;
20
+ padding: 1.25rem;
21
+ text-align: center;
22
+ }
23
+
24
+ .paddock-placeholder__label {
25
+ font: 800 0.72rem/1.2 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
26
+ letter-spacing: 0.14em;
27
+ text-transform: uppercase;
28
+ }
29
+
30
+ .paddock-placeholder__detail {
31
+ max-width: 32rem;
32
+ font: 500 0.82rem/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
33
+ opacity: 0.72;
34
+ }
35
+
36
+ .paddock-placeholder__lights {
37
+ display: grid;
38
+ grid-template-columns: repeat(5, 0.78rem);
39
+ gap: 0.42rem;
40
+ padding: 0.32rem 0.46rem;
41
+ border-radius: 999px;
42
+ background: rgba(255, 255, 255, 0.08);
43
+ }
44
+
45
+ .paddock-placeholder__lights span {
46
+ width: 0.78rem;
47
+ height: 0.78rem;
48
+ border-radius: 50%;
49
+ background: var(--paddock-placeholder-light-dim);
50
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);
51
+ animation: paddock-placeholder-lights 1200ms cubic-bezier(0.45, 0, 0.2, 1) infinite;
52
+ }
53
+
54
+ .paddock-placeholder__lights span:nth-child(2) { animation-delay: 120ms; }
55
+ .paddock-placeholder__lights span:nth-child(3) { animation-delay: 240ms; }
56
+ .paddock-placeholder__lights span:nth-child(4) { animation-delay: 360ms; }
57
+ .paddock-placeholder__lights span:nth-child(5) { animation-delay: 480ms; }
58
+
59
+ .paddock-placeholder[data-paddock-placeholder-variant="custom"] .paddock-placeholder__content {
60
+ min-height: inherit;
61
+ }
62
+
63
+ @keyframes paddock-placeholder-lights {
64
+ 0%, 100% {
65
+ background: var(--paddock-placeholder-light-dim);
66
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);
67
+ }
68
+ 45%, 70% {
69
+ background: var(--paddock-placeholder-light);
70
+ box-shadow: 0 0 14px rgba(225, 6, 0, 0.56);
71
+ }
72
+ }
@@ -1,35 +1,40 @@
1
1
  import {
2
- createVehicleGeometry,
3
- createVehicleGeometryState,
4
2
  getVehicleGeometryState,
5
- interpolateVehiclePose,
3
+ vehicleAxes,
6
4
  } from './vehicle/vehicleGeometry.js';
5
+ import { normalizeAngle } from './simMath.js';
7
6
 
8
7
  const DEFAULT_SWEEP_STEPS = 16;
9
8
  const BROADPHASE_PADDING = 0.001;
10
9
  const DEFAULT_DISTANCE_WINDOW = 150;
10
+ const COLLISION_PAIR_FIRST_INDEX = Symbol('collisionPairFirstIndex');
11
+ const COLLISION_PAIR_SECOND_INDEX = Symbol('collisionPairSecondIndex');
11
12
 
12
13
  function dot(a, b) {
13
14
  return a.x * b.x + a.y * b.y;
14
15
  }
15
16
 
16
- function projectShape(shape, axis) {
17
+ function projectShapeInto(target, shape, axis) {
17
18
  let min = Infinity;
18
19
  let max = -Infinity;
19
20
  for (let index = 0; index < shape.corners.length; index += 1) {
20
- const value = dot(shape.corners[index], axis);
21
+ const corner = shape.corners[index];
22
+ const value = corner.x * axis.x + corner.y * axis.y;
21
23
  if (value < min) min = value;
22
24
  if (value > max) max = value;
23
25
  }
24
- return {
25
- min,
26
- max,
27
- };
26
+ target.min = min;
27
+ target.max = max;
28
+ return target;
28
29
  }
29
30
 
30
- function overlapOnAxis(first, second, axis) {
31
- const a = projectShape(first, axis);
32
- const b = projectShape(second, axis);
31
+ function overlapOnAxis(first, second, axis, projectionScratch = null) {
32
+ const projections = projectionScratch ?? {
33
+ first: { min: Infinity, max: -Infinity },
34
+ second: { min: Infinity, max: -Infinity },
35
+ };
36
+ const a = projectShapeInto(projections.first, first, axis);
37
+ const b = projectShapeInto(projections.second, second, axis);
33
38
  return Math.min(a.max, b.max) - Math.max(a.min, b.min);
34
39
  }
35
40
 
@@ -37,58 +42,44 @@ function shapeContactType(firstShape, secondShape) {
37
42
  return `${firstShape.type}-${secondShape.type}`;
38
43
  }
39
44
 
40
- export function detectShapeCollision(firstShape, secondShape) {
41
- const axes = [
42
- firstShape.forward,
43
- firstShape.right,
44
- secondShape.forward,
45
- secondShape.right,
46
- ];
47
- let depth = Infinity;
48
- let axis = null;
49
-
50
- for (const candidate of axes) {
51
- const overlap = overlapOnAxis(firstShape, secondShape, candidate);
52
- if (overlap <= 0) return null;
53
- if (overlap < depth) {
54
- depth = overlap;
55
- axis = candidate;
56
- }
57
- }
45
+ export function detectShapeCollision(firstShape, secondShape, scratch = null) {
46
+ const projectionScratch = ensureProjectionScratch(scratch);
47
+ let axis = firstShape.forward;
48
+ let depth = overlapOnAxis(firstShape, secondShape, axis, projectionScratch);
49
+ if (depth <= 0) return null;
58
50
 
59
- const direction = {
60
- x: secondShape.center.x - firstShape.center.x,
61
- y: secondShape.center.y - firstShape.center.y,
62
- };
63
- if (dot(direction, axis) < 0) {
64
- axis = { x: -axis.x, y: -axis.y };
51
+ let overlap = overlapOnAxis(firstShape, secondShape, firstShape.right, projectionScratch);
52
+ if (overlap <= 0) return null;
53
+ if (overlap < depth) {
54
+ depth = overlap;
55
+ axis = firstShape.right;
65
56
  }
66
57
 
67
- return {
68
- axis,
69
- depth,
70
- firstShapeId: firstShape.id,
71
- secondShapeId: secondShape.id,
72
- contactType: shapeContactType(firstShape, secondShape),
73
- };
74
- }
58
+ overlap = overlapOnAxis(firstShape, secondShape, secondShape.forward, projectionScratch);
59
+ if (overlap <= 0) return null;
60
+ if (overlap < depth) {
61
+ depth = overlap;
62
+ axis = secondShape.forward;
63
+ }
75
64
 
76
- function collisionShapes(geometry) {
77
- return [geometry.body];
78
- }
65
+ overlap = overlapOnAxis(firstShape, secondShape, secondShape.right, projectionScratch);
66
+ if (overlap <= 0) return null;
67
+ if (overlap < depth) {
68
+ depth = overlap;
69
+ axis = secondShape.right;
70
+ }
79
71
 
80
- function detectGeometryCollision(firstGeometry, secondGeometry) {
81
- let best = null;
82
- collisionShapes(firstGeometry).forEach((firstShape) => {
83
- collisionShapes(secondGeometry).forEach((secondShape) => {
84
- const collision = detectShapeCollision(firstShape, secondShape);
85
- if (!collision) return;
86
- if (!best || collision.depth < best.depth - 1e-6) {
87
- best = collision;
88
- }
89
- });
90
- });
91
- return best;
72
+ const directionX = secondShape.center.x - firstShape.center.x;
73
+ const directionY = secondShape.center.y - firstShape.center.y;
74
+ const directionSign = directionX * axis.x + directionY * axis.y < 0 ? -1 : 1;
75
+ return writeShapeCollisionResult(
76
+ scratch ? (scratch.shapeCollisionResult ??= { axis: {} }) : { axis: {} },
77
+ firstShape,
78
+ secondShape,
79
+ axis.x * directionSign,
80
+ axis.y * directionSign,
81
+ depth,
82
+ );
92
83
  }
93
84
 
94
85
  function aabbsOverlap(first, second) {
@@ -104,34 +95,60 @@ function createSweptAabb(car) {
104
95
  return getVehicleGeometryState(car).sweptBodyAabb;
105
96
  }
106
97
 
107
- function withCollisionMetadata(collision, timeOfImpact, swept) {
108
- return {
109
- ...collision,
110
- timeOfImpact,
111
- swept: Boolean(swept),
112
- };
98
+ function writeShapeCollisionResult(target, firstShape, secondShape, axisX, axisY, depth) {
99
+ target.axis ??= {};
100
+ target.axis.x = axisX;
101
+ target.axis.y = axisY;
102
+ target.depth = depth;
103
+ target.firstShapeId = firstShape.id;
104
+ target.secondShapeId = secondShape.id;
105
+ target.contactType = shapeContactType(firstShape, secondShape);
106
+ return target;
107
+ }
108
+
109
+ function withCollisionMetadata(collision, timeOfImpact, swept, scratch) {
110
+ const target = scratch ? (scratch.vehicleCollisionResult ??= { axis: {} }) : { axis: {} };
111
+ target.axis ??= {};
112
+ target.axis.x = collision.axis.x;
113
+ target.axis.y = collision.axis.y;
114
+ target.depth = collision.depth;
115
+ target.firstShapeId = collision.firstShapeId;
116
+ target.secondShapeId = collision.secondShapeId;
117
+ target.contactType = collision.contactType;
118
+ target.timeOfImpact = timeOfImpact;
119
+ target.swept = Boolean(swept);
120
+ return target;
113
121
  }
114
122
 
115
- export function detectVehicleCollision(first, second, { sweepSteps = DEFAULT_SWEEP_STEPS } = {}) {
123
+ function detectVehicleCollisionWithScratch(first, second, sweepSteps, scratch) {
116
124
  const firstState = getVehicleGeometryState(first);
117
125
  const secondState = getVehicleGeometryState(second);
118
- const current = detectGeometryCollision(firstState.current, secondState.current);
119
- if (current) return withCollisionMetadata(current, 1, false);
126
+ const current = detectShapeCollision(firstState.current.body, secondState.current.body, scratch);
127
+ if (current) return withCollisionMetadata(current, 1, false, scratch);
120
128
 
121
129
  if (!aabbsOverlap(createSweptAabb(first), createSweptAabb(second))) return null;
122
130
 
131
+ const sweepShapes = ensureSweepShapes(scratch, firstState.current.body, secondState.current.body);
123
132
  for (let step = 1; step < sweepSteps; step += 1) {
124
133
  const amount = step / sweepSteps;
125
- const collision = detectGeometryCollision(
126
- createVehicleGeometry(interpolateVehiclePose(first, amount)),
127
- createVehicleGeometry(interpolateVehiclePose(second, amount)),
134
+ const collision = detectShapeCollision(
135
+ writeInterpolatedBodyShape(sweepShapes.first, firstState, amount),
136
+ writeInterpolatedBodyShape(sweepShapes.second, secondState, amount),
137
+ scratch,
128
138
  );
129
- if (collision) return withCollisionMetadata(collision, amount, true);
139
+ if (collision) return withCollisionMetadata(collision, amount, true, scratch);
130
140
  }
131
141
 
132
142
  return null;
133
143
  }
134
144
 
145
+ export function detectVehicleCollision(first, second, {
146
+ sweepSteps = DEFAULT_SWEEP_STEPS,
147
+ scratch = null,
148
+ } = {}) {
149
+ return detectVehicleCollisionWithScratch(first, second, sweepSteps, scratch);
150
+ }
151
+
135
152
  function normalizedDistance(car, trackLength) {
136
153
  if (!Number.isFinite(trackLength) || trackLength <= 0) return car.raceDistance;
137
154
  const distance = Number.isFinite(car.progress) ? car.progress : car.raceDistance;
@@ -147,34 +164,51 @@ function wrappedDistanceDelta(first, second, trackLength) {
147
164
  export function buildCollisionCandidatePairs(cars, {
148
165
  trackLength = null,
149
166
  distanceWindow = DEFAULT_DISTANCE_WINDOW,
167
+ scratch = null,
150
168
  } = {}) {
151
- const candidates = [];
152
- const candidateKeys = new Set();
153
- const withDistance = [];
154
- const withoutDistance = [];
155
- const carOrder = new Map(cars.map((car, index) => [car, index]));
156
-
157
- const addCandidate = (first, second) => {
158
- const firstIndex = carOrder.get(first);
159
- const secondIndex = carOrder.get(second);
160
- const key = firstIndex < secondIndex ? `${firstIndex}:${secondIndex}` : `${secondIndex}:${firstIndex}`;
161
- if (candidateKeys.has(key)) return;
162
- candidateKeys.add(key);
163
- candidates.push([first, second]);
169
+ const candidates = scratchArray(scratch, 'candidates');
170
+ const pairMarker = candidatePairMarker(scratch, cars.length);
171
+ const withDistance = scratchArray(scratch, 'withDistance');
172
+ const pairPool = scratchArray(scratch, 'pairPool', { clear: false });
173
+ const distanceEntryPool = scratchArray(scratch, 'distanceEntryPool', { clear: false });
174
+ const missingDistanceFlags = missingDistanceFlagScratch(scratch, cars.length);
175
+ const missingDistanceIndexes = scratchArray(scratch, 'missingDistanceIndexes');
176
+
177
+ const addCandidateByIndex = (firstIndex, secondIndex) => {
178
+ if (pairMarker.has(firstIndex, secondIndex)) return;
179
+ pairMarker.add(firstIndex, secondIndex);
180
+ const pair = pairPool[candidates.length] ?? [];
181
+ pair[0] = cars[firstIndex];
182
+ pair[1] = cars[secondIndex];
183
+ pair[COLLISION_PAIR_FIRST_INDEX] = firstIndex;
184
+ pair[COLLISION_PAIR_SECOND_INDEX] = secondIndex;
185
+ pairPool[candidates.length] = pair;
186
+ candidates.push(pair);
164
187
  };
165
188
 
166
- cars.forEach((car) => {
189
+ for (let index = 0; index < cars.length; index += 1) {
190
+ const car = cars[index];
191
+ missingDistanceFlags[index] = 0;
167
192
  const distance = normalizedDistance(car, trackLength);
168
- if (Number.isFinite(distance)) withDistance.push({ car, distance });
169
- else withoutDistance.push(car);
170
- });
193
+ if (Number.isFinite(distance)) {
194
+ const entry = distanceEntryPool[withDistance.length] ?? {};
195
+ entry.car = car;
196
+ entry.carIndex = index;
197
+ entry.distance = distance;
198
+ distanceEntryPool[withDistance.length] = entry;
199
+ withDistance.push(entry);
200
+ } else {
201
+ missingDistanceFlags[index] = 1;
202
+ missingDistanceIndexes.push(index);
203
+ }
204
+ }
171
205
 
172
206
  withDistance.sort((first, second) => first.distance - second.distance);
173
207
  for (let i = 0; i < withDistance.length; i += 1) {
174
208
  for (let j = i + 1; j < withDistance.length; j += 1) {
175
209
  const delta = withDistance[j].distance - withDistance[i].distance;
176
210
  if (delta > distanceWindow) break;
177
- addCandidate(withDistance[i].car, withDistance[j].car);
211
+ addCandidateByIndex(withDistance[i].carIndex, withDistance[j].carIndex);
178
212
  }
179
213
  }
180
214
 
@@ -184,17 +218,18 @@ export function buildCollisionCandidatePairs(cars, {
184
218
  const firstWrappedIndex = lowerBoundDistance(withDistance, threshold);
185
219
  for (let j = Math.max(firstWrappedIndex, i + 1); j < withDistance.length; j += 1) {
186
220
  if (wrappedDistanceDelta(withDistance[i].distance, withDistance[j].distance, trackLength) <= distanceWindow) {
187
- addCandidate(withDistance[i].car, withDistance[j].car);
221
+ addCandidateByIndex(withDistance[i].carIndex, withDistance[j].carIndex);
188
222
  }
189
223
  }
190
224
  }
191
225
  }
192
226
 
193
- const missingDistance = new Set(withoutDistance);
194
- for (let i = 0; i < cars.length; i += 1) {
195
- for (let j = i + 1; j < cars.length; j += 1) {
196
- if (missingDistance.has(cars[i]) || missingDistance.has(cars[j])) {
197
- addCandidate(cars[i], cars[j]);
227
+ if (missingDistanceIndexes.length > 0) {
228
+ for (let i = 0; i < cars.length; i += 1) {
229
+ for (let j = i + 1; j < cars.length; j += 1) {
230
+ if (missingDistanceFlags[i] || missingDistanceFlags[j]) {
231
+ addCandidateByIndex(i, j);
232
+ }
198
233
  }
199
234
  }
200
235
  }
@@ -202,6 +237,135 @@ export function buildCollisionCandidatePairs(cars, {
202
237
  return candidates;
203
238
  }
204
239
 
240
+ export function collisionCandidatePairFirstIndex(pair) {
241
+ return Number.isInteger(pair?.[COLLISION_PAIR_FIRST_INDEX])
242
+ ? pair[COLLISION_PAIR_FIRST_INDEX]
243
+ : -1;
244
+ }
245
+
246
+ export function collisionCandidatePairSecondIndex(pair) {
247
+ return Number.isInteger(pair?.[COLLISION_PAIR_SECOND_INDEX])
248
+ ? pair[COLLISION_PAIR_SECOND_INDEX]
249
+ : -1;
250
+ }
251
+
252
+ function scratchArray(scratch, key, { clear = true } = {}) {
253
+ const array = scratch ? (scratch[key] ??= []) : [];
254
+ if (clear) array.length = 0;
255
+ return array;
256
+ }
257
+
258
+ function candidatePairMarker(scratch, carCount) {
259
+ if (!scratch) return setBackedCandidatePairMarker(carCount);
260
+ const requiredLength = Math.max(1, carCount * carCount);
261
+ if (!(scratch.candidatePairMarks instanceof Uint32Array) || scratch.candidatePairMarks.length < requiredLength) {
262
+ scratch.candidatePairMarks = new Uint32Array(requiredLength);
263
+ }
264
+ scratch.candidatePairEpoch = ((scratch.candidatePairEpoch ?? 0) + 1) >>> 0;
265
+ if (scratch.candidatePairEpoch === 0) {
266
+ scratch.candidatePairMarks.fill(0);
267
+ scratch.candidatePairEpoch = 1;
268
+ }
269
+ const marks = scratch.candidatePairMarks;
270
+ const epoch = scratch.candidatePairEpoch;
271
+ return {
272
+ has(firstIndex, secondIndex) {
273
+ return marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] === epoch;
274
+ },
275
+ add(firstIndex, secondIndex) {
276
+ marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] = epoch;
277
+ },
278
+ };
279
+ }
280
+
281
+ function missingDistanceFlagScratch(scratch, carCount) {
282
+ if (!scratch) return new Uint8Array(Math.max(1, carCount));
283
+ if (!(scratch.missingDistanceFlags instanceof Uint8Array) || scratch.missingDistanceFlags.length < carCount) {
284
+ scratch.missingDistanceFlags = new Uint8Array(Math.max(1, carCount));
285
+ }
286
+ return scratch.missingDistanceFlags;
287
+ }
288
+
289
+ function setBackedCandidatePairMarker(carCount) {
290
+ const marks = new Uint32Array(Math.max(1, carCount * carCount));
291
+ return {
292
+ has(firstIndex, secondIndex) {
293
+ return marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] === 1;
294
+ },
295
+ add(firstIndex, secondIndex) {
296
+ marks[candidatePairMarkIndex(firstIndex, secondIndex, carCount)] = 1;
297
+ },
298
+ };
299
+ }
300
+
301
+ function candidatePairMarkIndex(firstIndex, secondIndex, carCount) {
302
+ const low = firstIndex < secondIndex ? firstIndex : secondIndex;
303
+ const high = firstIndex < secondIndex ? secondIndex : firstIndex;
304
+ return low * carCount + high;
305
+ }
306
+
307
+ function ensureSweepShapes(scratch, firstBody, secondBody) {
308
+ const sweepShapes = scratch ? (scratch.sweepShapes ??= {}) : {};
309
+ sweepShapes.first ??= createSweepShape(firstBody);
310
+ sweepShapes.second ??= createSweepShape(secondBody);
311
+ return sweepShapes;
312
+ }
313
+
314
+ function ensureProjectionScratch(scratch) {
315
+ const projectionScratch = scratch ? (scratch.projectionScratch ??= {}) : {};
316
+ projectionScratch.first ??= { min: Infinity, max: -Infinity };
317
+ projectionScratch.second ??= { min: Infinity, max: -Infinity };
318
+ return projectionScratch;
319
+ }
320
+
321
+ function createSweepShape(body) {
322
+ return {
323
+ id: body.id,
324
+ type: body.type,
325
+ center: { x: body.center.x, y: body.center.y },
326
+ heading: body.heading,
327
+ length: body.length,
328
+ width: body.width,
329
+ halfLength: body.halfLength,
330
+ halfWidth: body.halfWidth,
331
+ forward: { x: body.forward.x, y: body.forward.y },
332
+ right: { x: body.right.x, y: body.right.y },
333
+ corners: Array.from({ length: 4 }, () => ({ x: 0, y: 0 })),
334
+ };
335
+ }
336
+
337
+ function writeInterpolatedBodyShape(target, geometryState, amount) {
338
+ const pose = geometryState.pose;
339
+ const headingDelta = normalizeAngle(pose.heading - pose.previousHeading);
340
+ const x = pose.previousX + (pose.x - pose.previousX) * amount;
341
+ const y = pose.previousY + (pose.y - pose.previousY) * amount;
342
+ const heading = normalizeAngle(pose.previousHeading + headingDelta * amount);
343
+ const axes = vehicleAxes(heading);
344
+ const halfLength = target.halfLength;
345
+ const halfWidth = target.halfWidth;
346
+
347
+ target.center.x = x;
348
+ target.center.y = y;
349
+ target.heading = heading;
350
+ target.forward.x = axes.forward.x;
351
+ target.forward.y = axes.forward.y;
352
+ target.right.x = axes.right.x;
353
+ target.right.y = axes.right.y;
354
+
355
+ writeOffsetCorner(target.corners[0], target.center, target.forward, target.right, halfLength, halfWidth);
356
+ writeOffsetCorner(target.corners[1], target.center, target.forward, target.right, halfLength, -halfWidth);
357
+ writeOffsetCorner(target.corners[2], target.center, target.forward, target.right, -halfLength, -halfWidth);
358
+ writeOffsetCorner(target.corners[3], target.center, target.forward, target.right, -halfLength, halfWidth);
359
+
360
+ return target;
361
+ }
362
+
363
+ function writeOffsetCorner(target, center, forward, right, longitudinal, lateral) {
364
+ target.x = center.x + forward.x * longitudinal + right.x * lateral;
365
+ target.y = center.y + forward.y * longitudinal + right.y * lateral;
366
+ return target;
367
+ }
368
+
205
369
  function lowerBoundDistance(entries, target) {
206
370
  let low = 0;
207
371
  let high = entries.length;
@@ -78,9 +78,14 @@ export function canStartPitStop(sim, car) {
78
78
  const stop = car.pitStop;
79
79
  const pitStops = sim.rules.modules?.pitStops;
80
80
  if (!stop || !pitStops?.enabled) return false;
81
- const active = sim.cars.filter((candidate) => candidate !== car && isCarInActivePitStop(sim, candidate));
82
81
  const maxConcurrentPitLaneCars = Math.max(1, Math.floor(pitStops.maxConcurrentPitLaneCars ?? 3));
83
- if (active.length >= maxConcurrentPitLaneCars) return false;
82
+ let activeCount = 0;
83
+ for (let index = 0; index < sim.cars.length; index += 1) {
84
+ const candidate = sim.cars[index];
85
+ if (candidate === car || !isCarInActivePitStop(sim, candidate)) continue;
86
+ activeCount += 1;
87
+ if (activeCount >= maxConcurrentPitLaneCars) return false;
88
+ }
84
89
 
85
90
  if (!pitStops.doubleStacking && stop.teamId) {
86
91
  const box = getPitStopBox(sim, stop);
@@ -89,10 +94,13 @@ export function canStartPitStop(sim, car) {
89
94
 
90
95
  const minimumGap = Math.max(0, pitStops.minimumPitLaneGap ?? 0);
91
96
  const candidateDistance = car.raceDistance ?? 0;
92
- return active.every((candidate) => {
97
+ for (let index = 0; index < sim.cars.length; index += 1) {
98
+ const candidate = sim.cars[index];
99
+ if (candidate === car || !isCarInActivePitStop(sim, candidate)) continue;
93
100
  const gap = (candidate.raceDistance ?? 0) - candidateDistance;
94
- return gap < 0 || gap >= minimumGap;
95
- });
101
+ if (gap >= 0 && gap < minimumGap) return false;
102
+ }
103
+ return true;
96
104
  }
97
105
 
98
106
  export function shouldStartPitStop(sim, car) {
@@ -152,13 +160,17 @@ export function startPitStop(sim, car) {
152
160
  0,
153
161
  (hasServiceQueue ? queueDistanceAlongLane : stopDistanceAlongLane) - PIT_BOX_APPROACH_DISTANCE,
154
162
  );
155
- const route = createRoute([
156
- ...createPitApproachPoints(sim.track, car, pitLane, stop.entryRaceDistance),
157
- ...(pitLane.entry.roadCenterline ?? []).map((point) => routePoint(point)),
163
+ const routePoints = createPitApproachPoints(sim.track, car, pitLane, stop.entryRaceDistance);
164
+ const entryCenterline = pitLane.entry.roadCenterline ?? [];
165
+ for (let index = 0; index < entryCenterline.length; index += 1) {
166
+ routePoints.push(routePoint(entryCenterline[index]));
167
+ }
168
+ routePoints.push(
158
169
  routePoint(pitMainLanePointAt(pitLane, 0, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
159
170
  routePoint(pitMainLanePointAt(pitLane, boxApproachDistance, driveLaneOffset), pitLane.mainLane.heading, { limiterActive: true }),
160
171
  routePoint(serviceTarget, pitLane.mainLane.heading, { limiterActive: true }),
161
- ]);
172
+ );
173
+ const route = createRoute(routePoints);
162
174
  stop.status = 'entering';
163
175
  stop.phase = 'entry';
164
176
  stop.queueingForService = shouldStageForService;
@@ -34,11 +34,13 @@ export function beginPitQueue(sim, car, box) {
34
34
  export function releasePitQueue(sim, car, box, { fromCurrent = false } = {}) {
35
35
  const stop = car.pitStop;
36
36
  if (!stop || !box?.center) return false;
37
- const route = createRoute([
38
- ...(fromCurrent ? [routePoint(car, car.heading, { limiterActive: true })] : []),
37
+ const routePoints = [];
38
+ if (fromCurrent) routePoints.push(routePoint(car, car.heading, { limiterActive: true }));
39
+ routePoints.push(
39
40
  routePoint(box.queuePoint, sim.track.pitLane.mainLane.heading, { limiterActive: true }),
40
41
  routePoint(box.center, sim.track.pitLane.mainLane.heading, { limiterActive: true }),
41
- ]);
42
+ );
43
+ const route = createRoute(routePoints);
42
44
  stop.status = 'entering';
43
45
  stop.phase = 'queue-release';
44
46
  stop.queueingForService = false;