@inventure71/paddockjs 1.0.0 → 2.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.
- package/CHANGELOG.md +52 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +194 -12
- package/docs/architecture.md +119 -44
- package/docs/concepts.md +4 -2
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +299 -17
- package/docs/index.md +1 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +37 -10
- package/docs/system_specs.md +23 -10
- package/docs/training.md +272 -30
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/package.json +12 -4
- package/src/api/PaddockSimulatorController.js +4 -0
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +305 -1603
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +4 -0
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +20 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +361 -9
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observations.js +325 -40
- package/src/environment/options.js +37 -5
- package/src/environment/runtime.js +222 -21
- package/src/environment/scenarios.js +37 -8
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +65 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +100 -5
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +169 -6
- package/src/index.js +5 -0
- package/src/rendering/proceduralTrackAsset.js +20 -823
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +1 -1
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -619
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +92 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +230 -2817
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +141 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/trackLimitsSteward.js +1 -1
- package/src/simulation/rulesConfig.js +5 -323
- package/src/simulation/snapshots/raceSnapshots.js +192 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryIndex.js +945 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -1706
- package/src/simulation/vehicle/contactResolution.js +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -0
- package/src/simulation/vehicleGeometry.js +1 -247
- package/src/simulation/vehiclePhysics.js +1 -136
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -338
- package/src/styles.css +42 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +14 -360
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +5 -4
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/timingTowerTemplate.js +34 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GENERATED_FALLBACK_ATTEMPTS,
|
|
3
|
+
GENERATED_TRACK_ATTEMPTS,
|
|
4
|
+
GENERATED_TRACK_MAX_LENGTH,
|
|
5
|
+
GENERATED_TRACK_MIN_LENGTH,
|
|
6
|
+
MAX_LOCAL_TURN_RADIANS,
|
|
7
|
+
MAX_SAMPLE_HEADING_DELTA_RADIANS,
|
|
8
|
+
MIN_NON_ADJACENT_ARC_DISTANCE,
|
|
9
|
+
MIN_TRACK_CLEARANCE_MULTIPLIER,
|
|
10
|
+
MIN_TRACK_SHAPE_VARIATION,
|
|
11
|
+
START_STRAIGHT_BLEND_LENGTH,
|
|
12
|
+
START_STRAIGHT_EXIT_LENGTH,
|
|
13
|
+
START_STRAIGHT_GRID_LENGTH,
|
|
14
|
+
START_STRAIGHT_LOCK_EXTRA,
|
|
15
|
+
} from './trackConstants.js';
|
|
16
|
+
import { metersToSimUnits, simUnitsToMeters } from '../units.js';
|
|
17
|
+
|
|
18
|
+
export const PROCEDURAL_TRACK_PROFILES = {
|
|
19
|
+
race: {},
|
|
20
|
+
'training-short': {
|
|
21
|
+
length: { minMeters: 900, maxMeters: 1800 },
|
|
22
|
+
startStraight: { gridMeters: 0, exitMeters: 80, blendMeters: 80, lockExtraMeters: 0 },
|
|
23
|
+
pitLane: { enabled: false },
|
|
24
|
+
shape: { scale: 0.2, cornerDensity: 1.3, variation: 0.22 },
|
|
25
|
+
validation: {
|
|
26
|
+
minClearanceMultiplier: 1,
|
|
27
|
+
minNonAdjacentArcMeters: 160,
|
|
28
|
+
maxLocalTurnRadians: 1.85,
|
|
29
|
+
},
|
|
30
|
+
attempts: { primary: 80, fallback: 200 },
|
|
31
|
+
},
|
|
32
|
+
'training-medium': {
|
|
33
|
+
length: { minMeters: 1600, maxMeters: 3000 },
|
|
34
|
+
startStraight: { gridMeters: 80, exitMeters: 140, blendMeters: 120, lockExtraMeters: 0 },
|
|
35
|
+
pitLane: { enabled: false },
|
|
36
|
+
shape: { scale: 0.38, cornerDensity: 1.18, variation: 0.24 },
|
|
37
|
+
validation: {
|
|
38
|
+
minClearanceMultiplier: 1.15,
|
|
39
|
+
minNonAdjacentArcMeters: 260,
|
|
40
|
+
maxLocalTurnRadians: 1.7,
|
|
41
|
+
},
|
|
42
|
+
attempts: { primary: 80, fallback: 200 },
|
|
43
|
+
},
|
|
44
|
+
'training-technical': {
|
|
45
|
+
length: { minMeters: 800, maxMeters: 1700 },
|
|
46
|
+
startStraight: { gridMeters: 0, exitMeters: 60, blendMeters: 70, lockExtraMeters: 0 },
|
|
47
|
+
pitLane: { enabled: false },
|
|
48
|
+
shape: { scale: 0.18, cornerDensity: 1.55, variation: 0.2 },
|
|
49
|
+
validation: {
|
|
50
|
+
minClearanceMultiplier: 0.9,
|
|
51
|
+
minNonAdjacentArcMeters: 140,
|
|
52
|
+
maxLocalTurnRadians: 2.05,
|
|
53
|
+
},
|
|
54
|
+
attempts: { primary: 100, fallback: 240 },
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const RACE_DEFAULTS = {
|
|
59
|
+
profile: 'race',
|
|
60
|
+
length: {
|
|
61
|
+
min: GENERATED_TRACK_MIN_LENGTH,
|
|
62
|
+
max: GENERATED_TRACK_MAX_LENGTH,
|
|
63
|
+
},
|
|
64
|
+
startStraight: {
|
|
65
|
+
grid: START_STRAIGHT_GRID_LENGTH,
|
|
66
|
+
exit: START_STRAIGHT_EXIT_LENGTH,
|
|
67
|
+
blend: START_STRAIGHT_BLEND_LENGTH,
|
|
68
|
+
lockExtra: START_STRAIGHT_LOCK_EXTRA,
|
|
69
|
+
},
|
|
70
|
+
pitLane: {
|
|
71
|
+
enabled: true,
|
|
72
|
+
},
|
|
73
|
+
shape: {
|
|
74
|
+
scale: 1,
|
|
75
|
+
cornerDensity: 1,
|
|
76
|
+
variation: MIN_TRACK_SHAPE_VARIATION,
|
|
77
|
+
},
|
|
78
|
+
validation: {
|
|
79
|
+
minClearanceMultiplier: MIN_TRACK_CLEARANCE_MULTIPLIER,
|
|
80
|
+
minShapeVariation: MIN_TRACK_SHAPE_VARIATION,
|
|
81
|
+
minNonAdjacentArcDistance: MIN_NON_ADJACENT_ARC_DISTANCE,
|
|
82
|
+
maxLocalTurnRadians: MAX_LOCAL_TURN_RADIANS,
|
|
83
|
+
maxSampleHeadingDeltaRadians: MAX_SAMPLE_HEADING_DELTA_RADIANS,
|
|
84
|
+
},
|
|
85
|
+
attempts: {
|
|
86
|
+
primary: GENERATED_FALLBACK_ATTEMPTS,
|
|
87
|
+
fallback: GENERATED_TRACK_ATTEMPTS,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export function resolveProceduralTrackOptions(options = {}) {
|
|
92
|
+
const requestedProfile = options.profile ?? 'race';
|
|
93
|
+
if (!Object.hasOwn(PROCEDURAL_TRACK_PROFILES, requestedProfile)) {
|
|
94
|
+
throw new Error(`Unsupported procedural track profile: ${requestedProfile}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const expanded = expandLegacyTrackOptions(options);
|
|
98
|
+
const profile = expandLegacyTrackOptions(PROCEDURAL_TRACK_PROFILES[requestedProfile]);
|
|
99
|
+
const merged = deepMerge(deepMerge({ profile: requestedProfile }, profile), expanded);
|
|
100
|
+
|
|
101
|
+
const resolved = {
|
|
102
|
+
profile: requestedProfile,
|
|
103
|
+
length: {
|
|
104
|
+
min: positiveMetersToSimUnits(merged.length?.minMeters, RACE_DEFAULTS.length.min, 'length.minMeters'),
|
|
105
|
+
max: positiveMetersToSimUnits(merged.length?.maxMeters, RACE_DEFAULTS.length.max, 'length.maxMeters'),
|
|
106
|
+
},
|
|
107
|
+
startStraight: {
|
|
108
|
+
grid: nonNegativeMetersToSimUnits(merged.startStraight?.gridMeters, RACE_DEFAULTS.startStraight.grid, 'startStraight.gridMeters'),
|
|
109
|
+
exit: nonNegativeMetersToSimUnits(merged.startStraight?.exitMeters, RACE_DEFAULTS.startStraight.exit, 'startStraight.exitMeters'),
|
|
110
|
+
blend: nonNegativeMetersToSimUnits(merged.startStraight?.blendMeters, RACE_DEFAULTS.startStraight.blend, 'startStraight.blendMeters'),
|
|
111
|
+
lockExtra: nonNegativeMetersToSimUnits(merged.startStraight?.lockExtraMeters, RACE_DEFAULTS.startStraight.lockExtra, 'startStraight.lockExtraMeters'),
|
|
112
|
+
},
|
|
113
|
+
pitLane: {
|
|
114
|
+
enabled: merged.pitLane?.enabled !== false,
|
|
115
|
+
},
|
|
116
|
+
shape: {
|
|
117
|
+
scale: positiveNumber(merged.shape?.scale, RACE_DEFAULTS.shape.scale, 'shape.scale'),
|
|
118
|
+
cornerDensity: positiveNumber(merged.shape?.cornerDensity, RACE_DEFAULTS.shape.cornerDensity, 'shape.cornerDensity'),
|
|
119
|
+
variation: nonNegativeNumber(merged.shape?.variation, RACE_DEFAULTS.shape.variation, 'shape.variation'),
|
|
120
|
+
},
|
|
121
|
+
validation: {
|
|
122
|
+
minClearanceMultiplier: nonNegativeNumber(
|
|
123
|
+
merged.validation?.minClearanceMultiplier,
|
|
124
|
+
RACE_DEFAULTS.validation.minClearanceMultiplier,
|
|
125
|
+
'validation.minClearanceMultiplier',
|
|
126
|
+
),
|
|
127
|
+
minShapeVariation: nonNegativeNumber(
|
|
128
|
+
merged.validation?.minShapeVariation ?? merged.shape?.variation,
|
|
129
|
+
RACE_DEFAULTS.validation.minShapeVariation,
|
|
130
|
+
'validation.minShapeVariation',
|
|
131
|
+
),
|
|
132
|
+
minNonAdjacentArcDistance: nonNegativeMetersToSimUnits(
|
|
133
|
+
merged.validation?.minNonAdjacentArcMeters,
|
|
134
|
+
RACE_DEFAULTS.validation.minNonAdjacentArcDistance,
|
|
135
|
+
'validation.minNonAdjacentArcMeters',
|
|
136
|
+
),
|
|
137
|
+
maxLocalTurnRadians: positiveNumber(
|
|
138
|
+
merged.validation?.maxLocalTurnRadians,
|
|
139
|
+
RACE_DEFAULTS.validation.maxLocalTurnRadians,
|
|
140
|
+
'validation.maxLocalTurnRadians',
|
|
141
|
+
),
|
|
142
|
+
maxSampleHeadingDeltaRadians: positiveNumber(
|
|
143
|
+
merged.validation?.maxSampleHeadingDeltaRadians,
|
|
144
|
+
RACE_DEFAULTS.validation.maxSampleHeadingDeltaRadians,
|
|
145
|
+
'validation.maxSampleHeadingDeltaRadians',
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
attempts: {
|
|
149
|
+
primary: positiveInteger(merged.attempts?.primary, RACE_DEFAULTS.attempts.primary, 'attempts.primary'),
|
|
150
|
+
fallback: positiveInteger(merged.attempts?.fallback, RACE_DEFAULTS.attempts.fallback, 'attempts.fallback'),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (resolved.length.min > resolved.length.max) {
|
|
155
|
+
throw new Error('Procedural track length.minMeters must be less than or equal to length.maxMeters.');
|
|
156
|
+
}
|
|
157
|
+
resolved.cacheKey = stableTrackGenerationCacheKey(resolved);
|
|
158
|
+
return resolved;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function stableTrackGenerationCacheKey(options) {
|
|
162
|
+
return JSON.stringify({
|
|
163
|
+
profile: options.profile,
|
|
164
|
+
length: {
|
|
165
|
+
minMeters: roundMeters(options.length.min),
|
|
166
|
+
maxMeters: roundMeters(options.length.max),
|
|
167
|
+
},
|
|
168
|
+
startStraight: {
|
|
169
|
+
gridMeters: roundMeters(options.startStraight.grid),
|
|
170
|
+
exitMeters: roundMeters(options.startStraight.exit),
|
|
171
|
+
blendMeters: roundMeters(options.startStraight.blend),
|
|
172
|
+
lockExtraMeters: roundMeters(options.startStraight.lockExtra),
|
|
173
|
+
},
|
|
174
|
+
pitLane: { enabled: options.pitLane.enabled },
|
|
175
|
+
shape: {
|
|
176
|
+
scale: roundNumber(options.shape.scale),
|
|
177
|
+
cornerDensity: roundNumber(options.shape.cornerDensity),
|
|
178
|
+
variation: roundNumber(options.shape.variation),
|
|
179
|
+
},
|
|
180
|
+
validation: {
|
|
181
|
+
minClearanceMultiplier: roundNumber(options.validation.minClearanceMultiplier),
|
|
182
|
+
minShapeVariation: roundNumber(options.validation.minShapeVariation),
|
|
183
|
+
minNonAdjacentArcMeters: roundMeters(options.validation.minNonAdjacentArcDistance),
|
|
184
|
+
maxLocalTurnRadians: roundNumber(options.validation.maxLocalTurnRadians),
|
|
185
|
+
maxSampleHeadingDeltaRadians: roundNumber(options.validation.maxSampleHeadingDeltaRadians),
|
|
186
|
+
},
|
|
187
|
+
attempts: options.attempts,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function expandLegacyTrackOptions(options = {}) {
|
|
192
|
+
const next = { ...options };
|
|
193
|
+
if (options.minLengthMeters != null || options.maxLengthMeters != null) {
|
|
194
|
+
next.length = {
|
|
195
|
+
...(options.length ?? {}),
|
|
196
|
+
...(options.minLengthMeters != null ? { minMeters: options.minLengthMeters } : {}),
|
|
197
|
+
...(options.maxLengthMeters != null ? { maxMeters: options.maxLengthMeters } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (options.startStraightMeters != null) {
|
|
201
|
+
next.startStraight = {
|
|
202
|
+
...(options.startStraight ?? {}),
|
|
203
|
+
gridMeters: options.startStraightMeters,
|
|
204
|
+
exitMeters: options.startStraightMeters,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (options.includePitLane != null) {
|
|
208
|
+
next.pitLane = {
|
|
209
|
+
...(options.pitLane ?? {}),
|
|
210
|
+
enabled: Boolean(options.includePitLane),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return next;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function deepMerge(base, override) {
|
|
217
|
+
const result = { ...base };
|
|
218
|
+
Object.entries(override ?? {}).forEach(([key, value]) => {
|
|
219
|
+
if (isPlainObject(value) && isPlainObject(result[key])) {
|
|
220
|
+
result[key] = deepMerge(result[key], value);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (value !== undefined) result[key] = value;
|
|
224
|
+
});
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isPlainObject(value) {
|
|
229
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function positiveMetersToSimUnits(value, fallback, label) {
|
|
233
|
+
if (value == null) return fallback;
|
|
234
|
+
const number = Number(value);
|
|
235
|
+
if (!Number.isFinite(number) || number <= 0) {
|
|
236
|
+
throw new Error(`Procedural track ${label} must be a positive number.`);
|
|
237
|
+
}
|
|
238
|
+
return metersToSimUnits(number);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function nonNegativeMetersToSimUnits(value, fallback, label) {
|
|
242
|
+
if (value == null) return fallback;
|
|
243
|
+
const number = Number(value);
|
|
244
|
+
if (!Number.isFinite(number) || number < 0) {
|
|
245
|
+
throw new Error(`Procedural track ${label} must be a non-negative number.`);
|
|
246
|
+
}
|
|
247
|
+
return metersToSimUnits(number);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function positiveNumber(value, fallback, label) {
|
|
251
|
+
if (value == null) return fallback;
|
|
252
|
+
const number = Number(value);
|
|
253
|
+
if (!Number.isFinite(number) || number <= 0) {
|
|
254
|
+
throw new Error(`Procedural track ${label} must be a positive number.`);
|
|
255
|
+
}
|
|
256
|
+
return number;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function nonNegativeNumber(value, fallback, label) {
|
|
260
|
+
if (value == null) return fallback;
|
|
261
|
+
const number = Number(value);
|
|
262
|
+
if (!Number.isFinite(number) || number < 0) {
|
|
263
|
+
throw new Error(`Procedural track ${label} must be a non-negative number.`);
|
|
264
|
+
}
|
|
265
|
+
return number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function positiveInteger(value, fallback, label) {
|
|
269
|
+
if (value == null) return fallback;
|
|
270
|
+
const number = Number(value);
|
|
271
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
272
|
+
throw new Error(`Procedural track ${label} must be a positive integer.`);
|
|
273
|
+
}
|
|
274
|
+
return number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function roundMeters(value) {
|
|
278
|
+
return roundNumber(simUnitsToMeters(value));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function roundNumber(value) {
|
|
282
|
+
return Math.round(value * 1e6) / 1e6;
|
|
283
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { clamp } from '../simMath.js';
|
|
2
|
+
import { WORLD, PIT_WORLD_PADDING } from './trackConstants.js';
|
|
3
|
+
|
|
4
|
+
export function distance(a, b) {
|
|
5
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function orientation(a, b, c) {
|
|
9
|
+
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function segmentBoxesOverlap(a, b, c, d) {
|
|
13
|
+
return (
|
|
14
|
+
Math.max(Math.min(a.x, b.x), Math.min(c.x, d.x)) <= Math.min(Math.max(a.x, b.x), Math.max(c.x, d.x)) &&
|
|
15
|
+
Math.max(Math.min(a.y, b.y), Math.min(c.y, d.y)) <= Math.min(Math.max(a.y, b.y), Math.max(c.y, d.y))
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function segmentsIntersect(a, b, c, d) {
|
|
20
|
+
if (!segmentBoxesOverlap(a, b, c, d)) return false;
|
|
21
|
+
const abC = orientation(a, b, c);
|
|
22
|
+
const abD = orientation(a, b, d);
|
|
23
|
+
const cdA = orientation(c, d, a);
|
|
24
|
+
const cdB = orientation(c, d, b);
|
|
25
|
+
return abC * abD < 0 && cdA * cdB < 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function distanceForwardAlongTrack(from, to, totalLength) {
|
|
29
|
+
return to >= from ? to - from : totalLength - from + to;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function smoothstep(value) {
|
|
33
|
+
const amount = clamp(value, 0, 1);
|
|
34
|
+
return amount * amount * (3 - 2 * amount);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function blendPoint(original, target, amount) {
|
|
38
|
+
return {
|
|
39
|
+
...original,
|
|
40
|
+
x: original.x + (target.x - original.x) * amount,
|
|
41
|
+
y: original.y + (target.y - original.y) * amount,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function clonePoint(point) {
|
|
46
|
+
return {
|
|
47
|
+
x: point.x,
|
|
48
|
+
y: point.y,
|
|
49
|
+
heading: point.heading,
|
|
50
|
+
distance: point.distance,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeVector(vector) {
|
|
55
|
+
const length = Math.hypot(vector.x, vector.y) || 1;
|
|
56
|
+
return {
|
|
57
|
+
x: vector.x / length,
|
|
58
|
+
y: vector.y / length,
|
|
59
|
+
length,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function headingVector(heading) {
|
|
64
|
+
return {
|
|
65
|
+
x: Math.cos(heading),
|
|
66
|
+
y: Math.sin(heading),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function dotVectors(first, second) {
|
|
71
|
+
return first.x * second.x + first.y * second.y;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function angleBetweenVectors(first, second) {
|
|
75
|
+
const firstUnit = normalizeVector(first);
|
|
76
|
+
const secondUnit = normalizeVector(second);
|
|
77
|
+
return Math.acos(clamp(dotVectors(firstUnit, secondUnit), -1, 1));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function signedLateralOffsetToPoint(trackPoint, point) {
|
|
81
|
+
return (point.x - trackPoint.x) * trackPoint.normalX + (point.y - trackPoint.y) * trackPoint.normalY;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function interpolatePitPoint(start, forward, distanceAlong) {
|
|
85
|
+
return {
|
|
86
|
+
x: start.x + forward.x * distanceAlong,
|
|
87
|
+
y: start.y + forward.y * distanceAlong,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function projectPitPoint(startPoint, forward, normal, distanceAlong, lateralOffset) {
|
|
92
|
+
return {
|
|
93
|
+
x: startPoint.x + forward.x * distanceAlong + normal.x * lateralOffset,
|
|
94
|
+
y: startPoint.y + forward.y * distanceAlong + normal.y * lateralOffset,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function pointWorldClearance(point) {
|
|
99
|
+
return Math.min(
|
|
100
|
+
point.x - PIT_WORLD_PADDING,
|
|
101
|
+
WORLD.width - PIT_WORLD_PADDING - point.x,
|
|
102
|
+
point.y - PIT_WORLD_PADDING,
|
|
103
|
+
WORLD.height - PIT_WORLD_PADDING - point.y,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function sampleCubicBezier(p0, p1, p2, p3, steps) {
|
|
108
|
+
return Array.from({ length: steps + 1 }, (_, index) => {
|
|
109
|
+
const t = index / steps;
|
|
110
|
+
const mt = 1 - t;
|
|
111
|
+
const mt2 = mt * mt;
|
|
112
|
+
const t2 = t * t;
|
|
113
|
+
return {
|
|
114
|
+
x: mt2 * mt * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t2 * t * p3.x,
|
|
115
|
+
y: mt2 * mt * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t2 * t * p3.y,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function expandBounds(bounds, point) {
|
|
121
|
+
if (!point) return bounds;
|
|
122
|
+
return {
|
|
123
|
+
minX: Math.min(bounds.minX, point.x),
|
|
124
|
+
maxX: Math.max(bounds.maxX, point.x),
|
|
125
|
+
minY: Math.min(bounds.minY, point.y),
|
|
126
|
+
maxY: Math.max(bounds.maxY, point.y),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createPointBounds(points, padding = 0) {
|
|
131
|
+
const bounds = points.filter(Boolean).reduce(
|
|
132
|
+
(current, point) => expandBounds(current, point),
|
|
133
|
+
{ minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity },
|
|
134
|
+
);
|
|
135
|
+
if (!Number.isFinite(bounds.minX)) return null;
|
|
136
|
+
return {
|
|
137
|
+
minX: bounds.minX - padding,
|
|
138
|
+
maxX: bounds.maxX + padding,
|
|
139
|
+
minY: bounds.minY - padding,
|
|
140
|
+
maxY: bounds.maxY + padding,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function pointInsideBounds(point, bounds) {
|
|
145
|
+
if (!bounds) return true;
|
|
146
|
+
return (
|
|
147
|
+
point.x >= bounds.minX &&
|
|
148
|
+
point.x <= bounds.maxX &&
|
|
149
|
+
point.y >= bounds.minY &&
|
|
150
|
+
point.y <= bounds.maxY
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function projectPointToSegment(position, start, end) {
|
|
155
|
+
const dx = end.x - start.x;
|
|
156
|
+
const dy = end.y - start.y;
|
|
157
|
+
const lengthSquared = dx * dx + dy * dy;
|
|
158
|
+
const amount = lengthSquared > 0
|
|
159
|
+
? clamp(((position.x - start.x) * dx + (position.y - start.y) * dy) / lengthSquared, 0, 1)
|
|
160
|
+
: 0;
|
|
161
|
+
const projected = {
|
|
162
|
+
x: start.x + dx * amount,
|
|
163
|
+
y: start.y + dy * amount,
|
|
164
|
+
};
|
|
165
|
+
const length = Math.sqrt(lengthSquared);
|
|
166
|
+
const heading = Math.atan2(dy, dx);
|
|
167
|
+
const normalX = length > 0 ? -dy / length : 0;
|
|
168
|
+
const normalY = length > 0 ? dx / length : 1;
|
|
169
|
+
const signedOffset = (position.x - projected.x) * normalX + (position.y - projected.y) * normalY;
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
point: projected,
|
|
173
|
+
amount,
|
|
174
|
+
length,
|
|
175
|
+
heading,
|
|
176
|
+
normalX,
|
|
177
|
+
normalY,
|
|
178
|
+
signedOffset,
|
|
179
|
+
crossTrackError: Math.abs(signedOffset),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function nearestPointOnPolyline(points, position, segmentIndices = null) {
|
|
184
|
+
if (!Array.isArray(points) || points.length < 2) return null;
|
|
185
|
+
const restrictedSegments = Array.isArray(segmentIndices) && segmentIndices.length > 0
|
|
186
|
+
? [...new Set(segmentIndices)].filter((index) => Number.isInteger(index) && index >= 0 && index < points.length - 1)
|
|
187
|
+
: null;
|
|
188
|
+
const cumulativeDistances = restrictedSegments ? createPolylineCumulativeDistances(points) : null;
|
|
189
|
+
let best = null;
|
|
190
|
+
let distanceBefore = 0;
|
|
191
|
+
let totalLength = 0;
|
|
192
|
+
|
|
193
|
+
const segmentCount = restrictedSegments?.length ?? (points.length - 1);
|
|
194
|
+
for (let segmentOffset = 0; segmentOffset < segmentCount; segmentOffset += 1) {
|
|
195
|
+
const index = restrictedSegments ? restrictedSegments[segmentOffset] : segmentOffset;
|
|
196
|
+
const current = points[index];
|
|
197
|
+
const next = points[index + 1];
|
|
198
|
+
const projection = projectPointToSegment(position, current, next);
|
|
199
|
+
if (!best || projection.crossTrackError < best.crossTrackError) {
|
|
200
|
+
best = {
|
|
201
|
+
...projection,
|
|
202
|
+
segmentIndex: index,
|
|
203
|
+
distanceAlong: (restrictedSegments ? cumulativeDistances[index] : distanceBefore) + projection.length * projection.amount,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (!restrictedSegments) distanceBefore += projection.length;
|
|
207
|
+
totalLength += projection.length;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!best) return null;
|
|
211
|
+
return {
|
|
212
|
+
...best,
|
|
213
|
+
totalLength: restrictedSegments
|
|
214
|
+
? cumulativeDistances[cumulativeDistances.length - 1]
|
|
215
|
+
: totalLength,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function createPolylineCumulativeDistances(points) {
|
|
220
|
+
const distances = new Array(points.length).fill(0);
|
|
221
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
222
|
+
distances[index] = distances[index - 1] + distance(points[index - 1], points[index]);
|
|
223
|
+
}
|
|
224
|
+
return distances;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function pointIsInsidePolygon(point, polygon) {
|
|
228
|
+
if (!Array.isArray(polygon) || polygon.length < 3) return false;
|
|
229
|
+
let inside = false;
|
|
230
|
+
|
|
231
|
+
for (let currentIndex = 0, previousIndex = polygon.length - 1; currentIndex < polygon.length; previousIndex = currentIndex, currentIndex += 1) {
|
|
232
|
+
const current = polygon[currentIndex];
|
|
233
|
+
const previous = polygon[previousIndex];
|
|
234
|
+
const crossesY = (current.y > point.y) !== (previous.y > point.y);
|
|
235
|
+
if (!crossesY) continue;
|
|
236
|
+
const xAtY = ((previous.x - current.x) * (point.y - current.y)) / (previous.y - current.y) + current.x;
|
|
237
|
+
if (point.x < xAtY) inside = !inside;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return inside;
|
|
241
|
+
}
|