@inventure71/paddockjs 4.1.0 → 4.3.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 +76 -0
- package/INSTALL_AND_UPDATE.md +31 -5
- package/README.md +34 -3
- package/docs/advanced_ai_pace.md +131 -0
- package/docs/advanced_physics_rebuild.md +166 -0
- package/docs/architecture.md +111 -30
- package/docs/cleanup_coverage.md +284 -0
- package/docs/component_inventory.md +32 -16
- package/docs/composable-layouts.md +2 -0
- package/docs/concepts.md +12 -1
- package/docs/custom_model_controller.md +1 -1
- package/docs/data_contract.md +33 -10
- package/docs/data_model.md +4 -2
- package/docs/efficiency_cleanup_2026-10-13.md +80 -0
- package/docs/getting-started.md +3 -0
- package/docs/grid_walk.md +80 -0
- package/docs/index.md +5 -2
- package/docs/keyboard_driving_fix.md +118 -0
- package/docs/limit_driving_2026-10-14.md +88 -0
- package/docs/loading.md +141 -0
- package/docs/maintainability_audit_2026-09-08.md +146 -0
- package/docs/racing_ai_improvements.md +145 -0
- package/docs/rules.md +43 -13
- package/docs/sense_contract.md +4 -1
- package/docs/system_specs.md +40 -12
- package/docs/testing-reliability-guide.md +67 -0
- package/docs/v11_improvements.md +108 -0
- package/package.json +23 -15
- package/src/api/MountedSimulatorComponents.js +101 -0
- package/src/api/PaddockSimulatorController.js +56 -81
- package/src/app/BrowserExpertAdapter.js +0 -6
- package/src/app/BrowserExpertExternalRenderer.js +72 -50
- package/src/app/ExternalTrackSnapshot.js +50 -0
- package/src/app/F1SimulatorApp.js +242 -297
- package/src/app/HostLifecycleEmitter.js +93 -0
- package/src/app/banners/raceDataBannerController.js +9 -4
- package/src/app/camera/cameraConstants.js +1 -1
- package/src/app/camera/cameraController.js +10 -13
- package/src/app/camera/cameraGeometry.js +1 -1
- package/src/app/controls/SimulatorViewControls.js +45 -0
- package/src/app/controls/workbenchBindings.js +64 -0
- package/src/app/controls/workbenchDisclosure.js +41 -0
- package/src/app/domBindings.js +21 -13
- package/src/app/gridWalk/GridWalkController.js +180 -0
- package/src/app/gridWalk/gridWalkCamera.js +88 -0
- package/src/app/gridWalk/gridWalkControls.js +24 -0
- package/src/app/gridWalk/gridWalkCrew.js +246 -0
- package/src/app/gridWalk/gridWalkLabels.js +104 -0
- package/src/app/gridWalk/gridWalkLayout.js +46 -0
- package/src/app/gridWalk/gridWalkNavigation.js +99 -0
- package/src/app/gridWalk/gridWalkProjectBanner.js +43 -0
- package/src/app/gridWalk/gridWalkSession.js +50 -0
- package/src/app/layoutSupport.js +2 -0
- package/src/app/raceOverlayClearanceSupport.js +21 -7
- package/src/app/readouts/carOverviewRenderer.js +9 -4
- package/src/app/readouts/readoutFormatters.js +1 -8
- package/src/app/readouts/telemetryRenderer.js +1 -3
- package/src/app/readouts/timingTowerRenderer.js +48 -50
- package/src/app/rendering/appAssets.js +8 -10
- package/src/app/rendering/carRenderer.js +27 -79
- package/src/app/rendering/displayUtils.js +0 -6
- package/src/app/rendering/expertSensorRenderer.js +1 -1
- package/src/app/rendering/replayGhostRenderer.js +4 -1
- package/src/app/rendering/trackRenderer.js +2 -2
- package/src/app/runtime/frameLoop.js +18 -17
- package/src/app/runtime/pixiStartupLifecycle.js +94 -0
- package/src/app/runtime/runtimeTiming.js +4 -1
- package/src/app/runtime/runtimeVisibility.js +7 -2
- package/src/config/cssValues.js +1 -1
- package/src/config/defaultOptions.js +12 -173
- package/src/config/restartOptions.js +1 -0
- package/src/config/runtimeSeed.js +10 -0
- package/src/config/simulatorDefaults.js +106 -0
- package/src/config/telemetryModules.js +20 -0
- package/src/config/theme/cssTokens.js +85 -0
- package/src/config/theme/themeCss.js +72 -0
- package/src/config/theme/themeInputs.js +155 -0
- package/src/config/theme/themeResolver.js +172 -0
- package/src/config/theme/themeSelectors.js +69 -0
- package/src/config/theme/themeTokens.js +257 -0
- package/src/config/theme/themeUtils.js +15 -0
- package/src/config/themeOptions.js +14 -830
- package/src/config/timingGapMode.js +0 -2
- package/src/config/uiOptions.js +53 -0
- package/src/data/championship.js +16 -158
- package/src/data/championshipDefaults.js +87 -0
- package/src/data/driverData.js +10 -20
- package/src/data/entryIdentity.js +67 -0
- package/src/data/entryPresentation.js +23 -0
- package/src/data/entryTypes.d.ts +160 -0
- package/src/data/index.d.ts +12 -195
- package/src/data/ratingScale.js +14 -0
- package/src/data/teamTypes.d.ts +23 -0
- package/src/data/trackTypes.d.ts +101 -0
- package/src/data/vehicleData.js +12 -23
- package/src/environment/actions.js +3 -2
- package/src/environment/controllerLoop.js +57 -44
- package/src/environment/controllerScheduler.js +30 -0
- package/src/environment/driverObservations.js +260 -0
- package/src/environment/driverReset.js +51 -0
- package/src/environment/episode.js +0 -1
- package/src/environment/evaluation.js +6 -90
- package/src/environment/evaluationTracker.js +100 -0
- package/src/environment/events.js +23 -0
- package/src/environment/index.d.ts +35 -91
- package/src/environment/metrics.js +3 -17
- package/src/environment/observationSchema.js +104 -0
- package/src/environment/observationVector.js +16 -115
- package/src/environment/observations.js +16 -316
- package/src/environment/options.js +5 -24
- package/src/environment/renderFrames.js +21 -0
- package/src/environment/resetOptions.js +41 -0
- package/src/environment/results.js +125 -0
- package/src/environment/rewards.js +10 -16
- package/src/environment/runtime.js +17 -300
- package/src/environment/scenarioOptions.js +90 -0
- package/src/environment/scenarioPlacements.js +70 -0
- package/src/environment/scenarioPresets.js +78 -0
- package/src/environment/scenarios.js +5 -236
- package/src/environment/scratch.js +18 -0
- package/src/environment/sensorOptions.js +20 -0
- package/src/environment/sensors/index.js +2 -556
- package/src/environment/sensors/indexedRayBands.js +0 -18
- package/src/environment/sensors/nearbyCars.js +18 -15
- package/src/environment/sensors/opponentRadar.js +3 -2
- package/src/environment/sensors/pitConnectorProximity.js +20 -0
- package/src/environment/sensors/rayBandTrace.js +1 -37
- package/src/environment/sensors/rayBatch.js +81 -0
- package/src/environment/sensors/rayConfig.js +26 -0
- package/src/environment/sensors/rayGeometry.js +0 -8
- package/src/environment/sensors/rayGuards.js +0 -6
- package/src/environment/sensors/rayOutputs.js +75 -0
- package/src/environment/sensors/rayQuery.js +110 -0
- package/src/environment/sensors/rayScratch.js +146 -0
- package/src/environment/sensors/sensorTargets.js +12 -4
- package/src/environment/sensors/surfaceRays.js +6 -61
- package/src/environment/sensors/trackRays.js +7 -80
- package/src/environment/simulationSetup.js +30 -0
- package/src/environment/specs.js +15 -109
- package/src/index.d.ts +43 -202
- package/src/index.js +28 -6
- package/src/placeholder/index.js +1 -8
- package/src/rendering/displayLifecycle.js +6 -0
- package/src/rendering/proceduralTrackAsset.js +1 -1
- package/src/rendering/renderSnapshot.js +3 -0
- package/src/rendering/track/finishLineRenderer.js +1 -1
- package/src/rendering/track/gridRenderer.js +1 -1
- package/src/rendering/track/offsetStrokeSafety.js +2 -2
- package/src/rendering/track/pitLaneRenderer.js +1 -1
- package/src/rendering/track/trackMaterialRenderer.js +4 -4
- package/src/rendering/track/trackRenderConstants.js +0 -1
- package/src/rendering/track/trackRenderGeometry.js +1 -7
- package/src/simulation/collisionGeometry.js +12 -20
- package/src/simulation/driver/advancedPathControls.js +53 -0
- package/src/simulation/driver/advancedRacePace.js +112 -0
- package/src/simulation/driver/advancedRacingControls.js +61 -0
- package/src/simulation/driver/advancedRacingEnvelope.js +255 -0
- package/src/simulation/driver/advancedRacingSteering.js +76 -0
- package/src/simulation/driver/advancedRacingTiming.js +16 -0
- package/src/simulation/driver/arcadeRacePace.js +83 -0
- package/src/simulation/driver/arcadeRacingControls.js +111 -0
- package/src/simulation/driver/arcadeRacingEnvelope.js +63 -0
- package/src/simulation/driver/driverControlConstants.js +0 -2
- package/src/simulation/driver/driverController.js +16 -5
- package/src/simulation/driver/edgeRecovery.js +2 -3
- package/src/simulation/driver/racingControls.js +10 -396
- package/src/simulation/driver/racingDriverProfile.js +48 -0
- package/src/simulation/driver/racingLinePlan.js +54 -20
- package/src/simulation/driver/rejoinControls.js +59 -112
- package/src/simulation/driver/safetyCarControls.js +12 -0
- package/src/simulation/driver/trafficScan.js +136 -27
- package/src/simulation/participants/participantInteractions.js +24 -15
- package/src/simulation/pit/pitCarPlacement.js +29 -0
- package/src/simulation/pit/pitFlow.js +8 -30
- package/src/simulation/pit/pitIntent.js +4 -3
- package/src/simulation/pit/pitOccupancy.js +2 -2
- package/src/simulation/pit/pitQueue.js +2 -18
- package/src/simulation/pit/pitRouteMovement.js +1 -36
- package/src/simulation/pit/pitRouting.js +5 -81
- package/src/simulation/pit/pitService.js +3 -32
- package/src/simulation/pit/pitServiceConstants.js +1 -0
- package/src/simulation/pit/pitServiceProgress.js +34 -0
- package/src/simulation/pit/pitSnapshots.js +8 -4
- package/src/simulation/pit/pitState.js +1 -5
- package/src/simulation/race/classification.js +1 -8
- package/src/simulation/race/raceFinish.js +3 -8
- package/src/simulation/race/raceOrder.js +2 -13
- package/src/simulation/race/raceProgress.js +6 -20
- package/src/simulation/race/raceSetup.js +1 -1
- package/src/simulation/race/raceStep.js +20 -11
- package/src/simulation/race/redFlag.js +2 -4
- package/src/simulation/race/safetyCar.js +5 -9
- package/src/simulation/race/stalledDnf.js +2 -4
- package/src/simulation/raceSimulation.js +6 -38
- package/src/simulation/replay/replayGhosts.js +44 -32
- package/src/simulation/rules/collisionSteward.js +0 -14
- package/src/simulation/rules/penaltyActions.js +37 -0
- package/src/simulation/rules/penaltyLedger.js +8 -1
- package/src/simulation/rules/pitLaneSpeedingSteward.js +1 -1
- package/src/simulation/rules/rulesReview.js +20 -16
- package/src/simulation/rules/trackLimitsSteward.js +2 -2
- package/src/simulation/simMath.js +4 -0
- package/src/simulation/snapshots/raceSnapshots.js +30 -11
- package/src/simulation/timing/drsState.js +8 -0
- package/src/simulation/timing/drsTiming.js +2 -4
- package/src/simulation/timing/lapTelemetry.js +7 -4
- package/src/simulation/timing/sectorPerformance.js +14 -6
- package/src/simulation/timing/timingLines.js +12 -6
- package/src/simulation/track/pitLaneAccess.js +2 -2
- package/src/simulation/track/pitLaneLayout.js +4 -135
- package/src/simulation/track/pitLaneModel.js +70 -0
- package/src/simulation/track/pitLaneState.js +0 -1
- package/src/simulation/track/pitServiceLayout.js +121 -0
- package/src/simulation/track/proceduralTrack.js +77 -0
- package/src/simulation/track/spatialQueries.js +76 -31
- package/src/simulation/track/trackBuilder.js +54 -0
- package/src/simulation/track/trackConstants.js +0 -22
- package/src/simulation/track/trackMath.js +4 -29
- package/src/simulation/track/trackModel.js +12 -240
- package/src/simulation/track/trackModelCache.js +105 -0
- package/src/simulation/track/trackNearestQueries.js +1218 -0
- package/src/simulation/track/trackQueryConstants.js +3 -0
- package/src/simulation/track/trackQueryConstruction.js +368 -0
- package/src/simulation/track/trackQueryIndex.js +12 -1933
- package/src/simulation/track/trackQuerySegments.js +174 -0
- package/src/simulation/track/trackQueryStats.js +12 -0
- package/src/simulation/track/trackRayQueries.js +165 -0
- package/src/simulation/vehicle/advancedContactResponse.js +89 -0
- package/src/simulation/vehicle/advancedTireForces.js +92 -0
- package/src/simulation/vehicle/advancedVehicleModel.js +65 -0
- package/src/simulation/vehicle/advancedVehiclePhysics.js +127 -0
- package/src/simulation/vehicle/contactResolution.js +23 -9
- package/src/simulation/vehicle/mainTrackWheelSurface.js +2 -13
- package/src/simulation/vehicle/pitWheelSurface.js +0 -12
- package/src/simulation/vehicle/runoffResponse.js +2 -4
- package/src/simulation/vehicle/sampledWheelSurface.js +266 -0
- package/src/simulation/vehicle/vehicleGeometry.js +85 -103
- package/src/simulation/vehicle/vehicleKinematics.js +10 -0
- package/src/simulation/vehicle/vehiclePhysics.js +42 -196
- package/src/simulation/vehicle/vehicleSnapshots.js +18 -6
- package/src/simulation/vehicle/vehicleState.js +5 -3
- package/src/simulation/vehicle/wheelSurface.js +13 -432
- package/src/simulation/vehicle/wheelSurfaceCache.js +55 -0
- package/src/simulation/vehicle/wheelSurfaceSummary.js +101 -0
- package/src/styles.css +173 -19
- package/src/ui/bannerTemplates.js +4 -2
- package/src/ui/cameraControlsTemplate.js +4 -2
- package/src/ui/gridWalkTemplate.js +29 -0
- package/src/ui/htmlEscaping.js +8 -0
- package/src/ui/raceCanvasTemplate.js +3 -2
- package/src/ui/shellTemplate.js +3 -0
- package/src/ui/telemetryDrawerTemplate.js +4 -2
- package/src/ui/telemetryTemplates.js +3 -22
- package/src/ui/templateUtils.js +2 -8
- package/src/ui/viewControlsTemplate.js +21 -0
- package/examples/train-basic-policy.mjs +0 -276
- package/examples/trainingData.mjs +0 -133
|
@@ -3,27 +3,24 @@ import { FIXED_STEP } from '../../simulation/raceSimulation.js';
|
|
|
3
3
|
import { clamp } from '../../simulation/simMath.js';
|
|
4
4
|
import {
|
|
5
5
|
domUpdateIntervalForSpeed,
|
|
6
|
-
FRAME_PACING_EPSILON_MS,
|
|
7
6
|
maxSimulationStepsForFrame,
|
|
8
7
|
MAX_FRAME_CATCHUP_COUNT,
|
|
9
8
|
TARGET_FRAME_MS,
|
|
10
9
|
} from './runtimeTiming.js';
|
|
11
10
|
|
|
12
11
|
export function runFrameLoopTick(app) {
|
|
13
|
-
if (app.expertMode) return;
|
|
12
|
+
if (app.destroyed || app.expertMode) return;
|
|
13
|
+
const simulation = app.sim;
|
|
14
14
|
const now = performance.now();
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Math.floor((now - app.nextGameFrameTime + FRAME_PACING_EPSILON_MS) / TARGET_FRAME_MS) + 1,
|
|
19
|
-
1,
|
|
20
|
-
MAX_FRAME_CATCHUP_COUNT,
|
|
21
|
-
);
|
|
22
|
-
const frameSeconds = (TARGET_FRAME_MS * elapsedFrameCount) / 1000;
|
|
23
|
-
app.nextGameFrameTime += TARGET_FRAME_MS * elapsedFrameCount;
|
|
24
|
-
if (now - app.nextGameFrameTime > TARGET_FRAME_MS * MAX_FRAME_CATCHUP_COUNT) {
|
|
25
|
-
app.nextGameFrameTime = now + TARGET_FRAME_MS;
|
|
15
|
+
if (app.gridWalk?.tick(now)) {
|
|
16
|
+
app.lastTime = now;
|
|
17
|
+
return;
|
|
26
18
|
}
|
|
19
|
+
// Pixi owns draw pacing. A second frame gate can draw stale poses, and
|
|
20
|
+
// rounding wall time to whole frames destroys the interpolation remainder.
|
|
21
|
+
const elapsedMs = clamp(now - app.lastTime, 0, TARGET_FRAME_MS * MAX_FRAME_CATCHUP_COUNT);
|
|
22
|
+
const elapsedFrameCount = elapsedMs / TARGET_FRAME_MS;
|
|
23
|
+
const frameSeconds = elapsedMs / 1000;
|
|
27
24
|
app.lastTime = now;
|
|
28
25
|
app.sampleFps(now);
|
|
29
26
|
app.accumulator += frameSeconds * app.simulationSpeed;
|
|
@@ -31,23 +28,27 @@ export function runFrameLoopTick(app) {
|
|
|
31
28
|
let simulationSteps = 0;
|
|
32
29
|
const stepEvents = [];
|
|
33
30
|
const maxSimulationSteps = maxSimulationStepsForFrame(app.simulationSpeed, elapsedFrameCount);
|
|
34
|
-
while (app.accumulator >= FIXED_STEP && simulationSteps < maxSimulationSteps) {
|
|
31
|
+
while (app.accumulator >= FIXED_STEP - 1e-10 && simulationSteps < maxSimulationSteps) {
|
|
35
32
|
app.sim.step(FIXED_STEP);
|
|
36
33
|
if (Array.isArray(app.sim.events) && app.sim.events.length > 0) {
|
|
37
34
|
stepEvents.push(...app.sim.events);
|
|
38
35
|
}
|
|
39
|
-
app.accumulator
|
|
36
|
+
app.accumulator = Math.max(0, app.accumulator - FIXED_STEP);
|
|
40
37
|
simulationSteps += 1;
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
if (app.accumulator >= FIXED_STEP) {
|
|
44
41
|
app.accumulator %= FIXED_STEP;
|
|
45
|
-
app.nextGameFrameTime = now + TARGET_FRAME_MS;
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
const shouldUpdateDom = now - app.lastDomUpdateTime >= domUpdateIntervalForSpeed(app.simulationSpeed);
|
|
49
45
|
const fullSnapshot = stepEvents.length > 0 || shouldUpdateDom ? app.sim.snapshot() : null;
|
|
50
46
|
if (stepEvents.length > 0) app.emitRaceEvents(stepEvents, fullSnapshot);
|
|
47
|
+
// Host lifecycle callbacks may synchronously unmount or restart the race.
|
|
48
|
+
// Do not render a disposed scene or deliver the old snapshot to its successor.
|
|
49
|
+
if (app.destroyed || app.sim !== simulation) return;
|
|
50
|
+
if (fullSnapshot) app.emitSnapshotLifecycle(fullSnapshot);
|
|
51
|
+
if (app.destroyed || app.sim !== simulation) return;
|
|
51
52
|
const renderSource = fullSnapshot ??
|
|
52
53
|
app.sim.snapshotRenderInto?.(app.renderSourceSnapshotBuffer ?? {}) ??
|
|
53
54
|
app.sim.snapshotRender?.() ??
|
|
@@ -59,7 +60,7 @@ export function runFrameLoopTick(app) {
|
|
|
59
60
|
clamp(app.accumulator / FIXED_STEP, 0, 1),
|
|
60
61
|
);
|
|
61
62
|
app.renderSnapshotBuffer = renderSnapshot;
|
|
62
|
-
app.applyCamera(renderSnapshot);
|
|
63
|
+
app.applyCamera(renderSnapshot, { elapsedMs });
|
|
63
64
|
app.renderDrsTrails(renderSnapshot);
|
|
64
65
|
app.renderPitLaneStatus(renderSnapshot);
|
|
65
66
|
app.renderCars(renderSnapshot);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ResizePlugin, TickerPlugin } from 'pixi.js';
|
|
2
|
+
|
|
3
|
+
export class RuntimeStartupCancelledError extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super('PaddockJS initialization was cancelled before completion.');
|
|
6
|
+
this.name = 'RuntimeStartupCancelledError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function assertRuntimeStartupActive(app) {
|
|
11
|
+
if (app.destroyed) throw new RuntimeStartupCancelledError();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function releaseInitializedPixiPlugins(application, onCleanupError) {
|
|
15
|
+
// Runtime init owns Pixi's private ticker and resize target. Init can fail
|
|
16
|
+
// between these plugins, so calling Application.destroy() is unsafe here.
|
|
17
|
+
if (application.ticker) {
|
|
18
|
+
try {
|
|
19
|
+
TickerPlugin.destroy.call(application);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
onCleanupError(error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (typeof application._cancelResize === 'function') {
|
|
25
|
+
try {
|
|
26
|
+
ResizePlugin.destroy.call(application);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
onCleanupError(error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pixi's application plugins cannot be destroyed until their async init has settled.
|
|
34
|
+
export function createPixiStartupLifecycle(application, onCleanupError = () => {}) {
|
|
35
|
+
let initializing = false;
|
|
36
|
+
let initialized = false;
|
|
37
|
+
let releaseRequested = false;
|
|
38
|
+
let released = false;
|
|
39
|
+
|
|
40
|
+
function release() {
|
|
41
|
+
if (initializing || released) return;
|
|
42
|
+
released = true;
|
|
43
|
+
let rendererCleanupNeeded = true;
|
|
44
|
+
if (initialized) {
|
|
45
|
+
try {
|
|
46
|
+
application.destroy(true, { children: true, texture: false });
|
|
47
|
+
return;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Pixi clears stage immediately before calling renderer.destroy().
|
|
50
|
+
// A renderer failure must be reported without invoking that disposer twice.
|
|
51
|
+
rendererCleanupNeeded = application.stage != null;
|
|
52
|
+
onCleanupError(error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Renderer discovery or an application plugin can fail before all plugins
|
|
56
|
+
// are installed. Release only the instance-owned plugins that reached init.
|
|
57
|
+
releaseInitializedPixiPlugins(application, onCleanupError);
|
|
58
|
+
try {
|
|
59
|
+
if (!application.stage?.destroyed) application.stage?.destroy?.({ children: true, texture: false });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
onCleanupError(error);
|
|
62
|
+
} finally {
|
|
63
|
+
application.stage = null;
|
|
64
|
+
try {
|
|
65
|
+
if (rendererCleanupNeeded) application.renderer?.destroy?.(true);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
onCleanupError(error);
|
|
68
|
+
} finally {
|
|
69
|
+
application.renderer = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
async initialize(options) {
|
|
76
|
+
initializing = true;
|
|
77
|
+
try {
|
|
78
|
+
await application.init(options);
|
|
79
|
+
initialized = true;
|
|
80
|
+
// Pixi defaults to owning touch gestures. Let the host page scroll;
|
|
81
|
+
// Grid Walk opts into dragging through its own scoped touch-action CSS.
|
|
82
|
+
if (application.renderer.events) application.renderer.events.autoPreventDefault = false;
|
|
83
|
+
application.canvas.style?.removeProperty('touch-action');
|
|
84
|
+
} finally {
|
|
85
|
+
initializing = false;
|
|
86
|
+
if (releaseRequested) release();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
destroy() {
|
|
90
|
+
releaseRequested = true;
|
|
91
|
+
release();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -2,13 +2,16 @@ import { clamp } from '../../simulation/simMath.js';
|
|
|
2
2
|
|
|
3
3
|
export const TARGET_RENDER_FPS = 60;
|
|
4
4
|
export const TARGET_FRAME_MS = 1000 / TARGET_RENDER_FPS;
|
|
5
|
-
export const FRAME_PACING_EPSILON_MS = 0.75;
|
|
6
5
|
export const MAX_FRAME_CATCHUP_COUNT = 4;
|
|
7
6
|
export const BASE_MAX_SIMULATION_STEPS_PER_RENDER = 5;
|
|
8
7
|
export const HARD_MAX_SIMULATION_STEPS_PER_RENDER = 60;
|
|
9
8
|
export const DOM_UPDATE_INTERVAL_MS = 100;
|
|
10
9
|
export const TIMING_UPDATE_INTERVAL_MS = 250;
|
|
11
10
|
|
|
11
|
+
export function configureRuntimeTicker(ticker) {
|
|
12
|
+
if (ticker) ticker.maxFPS = TARGET_RENDER_FPS;
|
|
13
|
+
}
|
|
14
|
+
|
|
12
15
|
export function maxSimulationStepsForFrame(simulationSpeed, elapsedFrameCount) {
|
|
13
16
|
const speed = Math.max(1, Number(simulationSpeed) || 1);
|
|
14
17
|
const frameCount = Math.max(1, Number(elapsedFrameCount) || 1);
|
|
@@ -68,7 +68,8 @@ export function syncRuntimeTicker(app) {
|
|
|
68
68
|
const ticker = app.app?.ticker;
|
|
69
69
|
if (!ticker) return;
|
|
70
70
|
|
|
71
|
-
const shouldRun = app.runtimeViewportVisible && app.runtimeDocumentVisible
|
|
71
|
+
const shouldRun = app.runtimeViewportVisible && app.runtimeDocumentVisible &&
|
|
72
|
+
(!app.gridWalk?.active || app.gridWalk.needsAnimation);
|
|
72
73
|
const tickerRunning = typeof ticker.started === 'boolean'
|
|
73
74
|
? ticker.started
|
|
74
75
|
: app.runtimeTickerRunning;
|
|
@@ -76,7 +77,11 @@ export function syncRuntimeTicker(app) {
|
|
|
76
77
|
|
|
77
78
|
app.runtimeTickerRunning = shouldRun;
|
|
78
79
|
if (shouldRun) {
|
|
79
|
-
app.
|
|
80
|
+
if (app.gridWalk) {
|
|
81
|
+
// Grid Walk pauses retain the fractional fixed step across ticker stops.
|
|
82
|
+
app.lastTime = performance.now();
|
|
83
|
+
app.gridWalk.lastTime = app.lastTime;
|
|
84
|
+
} else app.resetFrameClock();
|
|
80
85
|
ticker.start?.();
|
|
81
86
|
} else {
|
|
82
87
|
ticker.stop?.();
|
package/src/config/cssValues.js
CHANGED
|
@@ -1,124 +1,17 @@
|
|
|
1
|
+
import { DEFAULT_F1_SIMULATOR_OPTIONS, PADDOCK_SIMULATOR_PRESETS } from './simulatorDefaults.js';
|
|
2
|
+
import { resolveSimulatorUiOptions } from './uiOptions.js';
|
|
3
|
+
export { DEFAULT_F1_SIMULATOR_OPTIONS, PADDOCK_SIMULATOR_PRESETS } from './simulatorDefaults.js';
|
|
1
4
|
import { resolveF1SimulatorAssets } from './defaultAssets.js';
|
|
2
5
|
import { CHAMPIONSHIP_ENTRY_BLUEPRINTS } from '../data/championship.js';
|
|
3
6
|
import { normalizeSimulatorDrivers } from '../data/normalizeDrivers.js';
|
|
4
7
|
import { normalizePhysicsMode } from '../simulation/vehicle/vehiclePhysics.js';
|
|
5
8
|
import { normalizeWarmupOptions } from '../simulation/warmup/runtimeWarmup.js';
|
|
6
9
|
import {
|
|
7
|
-
DEFAULT_PADDOCK_THEME_INPUT,
|
|
8
|
-
applyPaddockThemeCssVariables,
|
|
9
10
|
mergeThemeInputs,
|
|
10
11
|
normalizePaddockTheme,
|
|
11
12
|
} from './themeOptions.js';
|
|
12
|
-
import { normalizeTimingGapMode } from './timingGapMode.js';
|
|
13
13
|
import { normalizePublicUrlOption } from './urlOptions.js';
|
|
14
14
|
|
|
15
|
-
export const DEFAULT_F1_SIMULATOR_OPTIONS = {
|
|
16
|
-
seed: 1971,
|
|
17
|
-
physicsMode: 'arcade',
|
|
18
|
-
warmup: {
|
|
19
|
-
enabled: true,
|
|
20
|
-
policy: 'config-change',
|
|
21
|
-
},
|
|
22
|
-
totalLaps: 10,
|
|
23
|
-
initialCameraMode: 'leader',
|
|
24
|
-
title: 'F1 Simulator Lab',
|
|
25
|
-
kicker: 'Race Control',
|
|
26
|
-
backLinkHref: 'projects.html',
|
|
27
|
-
backLinkLabel: 'Projects',
|
|
28
|
-
showBackLink: true,
|
|
29
|
-
ui: {
|
|
30
|
-
layoutPreset: 'standard',
|
|
31
|
-
cameraControls: 'external',
|
|
32
|
-
showFps: true,
|
|
33
|
-
showTimingTower: true,
|
|
34
|
-
showTelemetry: true,
|
|
35
|
-
telemetryIncludesOverview: true,
|
|
36
|
-
telemetryModules: {
|
|
37
|
-
core: true,
|
|
38
|
-
sectors: true,
|
|
39
|
-
lapTimes: true,
|
|
40
|
-
sectorTimes: true,
|
|
41
|
-
},
|
|
42
|
-
showRaceDataPanel: true,
|
|
43
|
-
raceDataTelemetryDetail: false,
|
|
44
|
-
driverCamera: false,
|
|
45
|
-
raceDataBanners: {
|
|
46
|
-
initial: 'project',
|
|
47
|
-
enabled: ['project', 'radio'],
|
|
48
|
-
},
|
|
49
|
-
raceDataBannerSize: 'custom',
|
|
50
|
-
timingTowerVerticalFit: 'expand-race-view',
|
|
51
|
-
timingGapMode: 'interval',
|
|
52
|
-
timingGapModeToggle: true,
|
|
53
|
-
timingEntryVerticalPadding: 5,
|
|
54
|
-
responsiveNarrowLayout: true,
|
|
55
|
-
},
|
|
56
|
-
debug: {
|
|
57
|
-
physicsModeIndicator: false,
|
|
58
|
-
},
|
|
59
|
-
theme: DEFAULT_PADDOCK_THEME_INPUT,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const PADDOCK_SIMULATOR_PRESETS = {
|
|
63
|
-
dashboard: {
|
|
64
|
-
ui: {
|
|
65
|
-
layoutPreset: 'standard',
|
|
66
|
-
cameraControls: 'external',
|
|
67
|
-
showFps: true,
|
|
68
|
-
showTimingTower: true,
|
|
69
|
-
showTelemetry: true,
|
|
70
|
-
showRaceDataPanel: true,
|
|
71
|
-
raceDataBannerSize: 'custom',
|
|
72
|
-
timingTowerVerticalFit: 'expand-race-view',
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
'timing-overlay': {
|
|
76
|
-
ui: {
|
|
77
|
-
layoutPreset: 'left-tower-overlay',
|
|
78
|
-
cameraControls: 'external',
|
|
79
|
-
showFps: false,
|
|
80
|
-
showTimingTower: true,
|
|
81
|
-
showTelemetry: true,
|
|
82
|
-
showRaceDataPanel: true,
|
|
83
|
-
raceDataBannerSize: 'auto',
|
|
84
|
-
timingTowerVerticalFit: 'expand-race-view',
|
|
85
|
-
raceDataBanners: {
|
|
86
|
-
initial: 'project',
|
|
87
|
-
enabled: ['project', 'radio'],
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
'compact-race': {
|
|
92
|
-
ui: {
|
|
93
|
-
layoutPreset: 'standard',
|
|
94
|
-
cameraControls: 'external',
|
|
95
|
-
showFps: false,
|
|
96
|
-
showTimingTower: false,
|
|
97
|
-
showTelemetry: false,
|
|
98
|
-
showRaceDataPanel: true,
|
|
99
|
-
raceDataBannerSize: 'auto',
|
|
100
|
-
timingTowerVerticalFit: 'scroll',
|
|
101
|
-
},
|
|
102
|
-
theme: {
|
|
103
|
-
timingTowerMaxWidth: '340px',
|
|
104
|
-
raceViewMinHeight: '460px',
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
'full-dashboard': {
|
|
108
|
-
ui: {
|
|
109
|
-
layoutPreset: 'standard',
|
|
110
|
-
cameraControls: 'external',
|
|
111
|
-
showFps: true,
|
|
112
|
-
showTimingTower: true,
|
|
113
|
-
showTelemetry: true,
|
|
114
|
-
telemetryIncludesOverview: true,
|
|
115
|
-
showRaceDataPanel: true,
|
|
116
|
-
raceDataBannerSize: 'custom',
|
|
117
|
-
timingTowerVerticalFit: 'expand-race-view',
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
|
|
122
15
|
export {
|
|
123
16
|
DEFAULT_PADDOCK_THEME,
|
|
124
17
|
PADDOCK_THEME_CSS_VARIABLES,
|
|
@@ -130,12 +23,10 @@ export {
|
|
|
130
23
|
|
|
131
24
|
const SUPPORTED_CAMERA_MODES = new Set(['overview', 'leader', 'selected', 'driver', 'show-all', 'pit']);
|
|
132
25
|
|
|
133
|
-
function normalizeNonNegativeFiniteNumber(value, fallback) {
|
|
134
|
-
const numericValue = Number(value);
|
|
135
|
-
return Number.isFinite(numericValue) && numericValue >= 0 ? numericValue : fallback;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
26
|
export function resolveF1SimulatorOptions(options = {}) {
|
|
27
|
+
if (options.gridWalk?.enabled && options.expert?.enabled) {
|
|
28
|
+
throw new Error('Grid Walk requires normal browser playback; expert mode owns its own simulation lifecycle.');
|
|
29
|
+
}
|
|
139
30
|
const presetName = Object.hasOwn(PADDOCK_SIMULATOR_PRESETS, options.preset)
|
|
140
31
|
? options.preset
|
|
141
32
|
: null;
|
|
@@ -145,39 +36,7 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
145
36
|
const initialCameraMode = SUPPORTED_CAMERA_MODES.has(options.initialCameraMode)
|
|
146
37
|
? options.initialCameraMode
|
|
147
38
|
: DEFAULT_F1_SIMULATOR_OPTIONS.initialCameraMode;
|
|
148
|
-
const ui =
|
|
149
|
-
...DEFAULT_F1_SIMULATOR_OPTIONS.ui,
|
|
150
|
-
...presetUi,
|
|
151
|
-
...requestedUi,
|
|
152
|
-
raceDataBanners: {
|
|
153
|
-
...DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners,
|
|
154
|
-
...(presetUi.raceDataBanners ?? {}),
|
|
155
|
-
...(requestedUi.raceDataBanners ?? {}),
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
ui.driverCamera = Boolean(ui.driverCamera || initialCameraMode === 'driver');
|
|
159
|
-
ui.raceDataBanners.enabled = normalizeEnabledBanners(ui.raceDataBanners.enabled);
|
|
160
|
-
ui.telemetryModules = normalizeTelemetryModules(ui.telemetryModules);
|
|
161
|
-
if (!['project', 'radio', 'hidden'].includes(ui.raceDataBanners.initial)) {
|
|
162
|
-
ui.raceDataBanners.initial = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.initial;
|
|
163
|
-
}
|
|
164
|
-
if (ui.raceDataBanners.initial !== 'hidden' && !ui.raceDataBanners.enabled.includes(ui.raceDataBanners.initial)) {
|
|
165
|
-
ui.raceDataBanners.initial = 'hidden';
|
|
166
|
-
}
|
|
167
|
-
if (!['expand-race-view', 'scroll'].includes(ui.timingTowerVerticalFit)) {
|
|
168
|
-
ui.timingTowerVerticalFit = DEFAULT_F1_SIMULATOR_OPTIONS.ui.timingTowerVerticalFit;
|
|
169
|
-
}
|
|
170
|
-
ui.timingGapMode = normalizeTimingGapMode(ui.timingGapMode);
|
|
171
|
-
ui.timingGapModeToggle = ui.timingGapModeToggle !== false;
|
|
172
|
-
ui.timingEntryVerticalPadding = normalizeNonNegativeFiniteNumber(
|
|
173
|
-
ui.timingEntryVerticalPadding,
|
|
174
|
-
DEFAULT_F1_SIMULATOR_OPTIONS.ui.timingEntryVerticalPadding,
|
|
175
|
-
);
|
|
176
|
-
ui.responsiveNarrowLayout = ui.responsiveNarrowLayout !== false;
|
|
177
|
-
if (!['auto', 'custom'].includes(ui.raceDataBannerSize)) {
|
|
178
|
-
ui.raceDataBannerSize = DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBannerSize;
|
|
179
|
-
}
|
|
180
|
-
ui.raceDataTelemetryDetail = Boolean(ui.raceDataTelemetryDetail);
|
|
39
|
+
const ui = resolveSimulatorUiOptions(requestedUi, presetUi, initialCameraMode);
|
|
181
40
|
const debug = {
|
|
182
41
|
...DEFAULT_F1_SIMULATOR_OPTIONS.debug,
|
|
183
42
|
...(preset.debug ?? {}),
|
|
@@ -211,6 +70,11 @@ export function resolveF1SimulatorOptions(options = {}) {
|
|
|
211
70
|
physicsMode: normalizePhysicsMode(options.physicsMode ?? preset.physicsMode),
|
|
212
71
|
warmup: normalizeWarmupOptions(mergedOptions.warmup, 'browser'),
|
|
213
72
|
initialCameraMode,
|
|
73
|
+
gridWalk: {
|
|
74
|
+
enabled: options.gridWalk?.enabled === true,
|
|
75
|
+
initialView: options.gridWalk?.initialView === 'race' ? 'race' : 'grid-walk',
|
|
76
|
+
ambientMotion: options.gridWalk?.ambientMotion !== false,
|
|
77
|
+
},
|
|
214
78
|
ui,
|
|
215
79
|
debug,
|
|
216
80
|
theme,
|
|
@@ -230,28 +94,3 @@ function collectDriverTeamThemes(drivers = []) {
|
|
|
230
94
|
});
|
|
231
95
|
return Object.keys(teamThemes).length > 0 ? { teamThemes } : {};
|
|
232
96
|
}
|
|
233
|
-
|
|
234
|
-
function normalizeEnabledBanners(value) {
|
|
235
|
-
if (value === false) return [];
|
|
236
|
-
if (value === true || value == null) return [...DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.enabled];
|
|
237
|
-
if (!Array.isArray(value)) return [...DEFAULT_F1_SIMULATOR_OPTIONS.ui.raceDataBanners.enabled];
|
|
238
|
-
return [...new Set(value.filter((item) => item === 'project' || item === 'radio'))];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function normalizeTelemetryModules(value) {
|
|
242
|
-
const defaults = DEFAULT_F1_SIMULATOR_OPTIONS.ui.telemetryModules;
|
|
243
|
-
const names = Object.keys(defaults);
|
|
244
|
-
if (value === false) {
|
|
245
|
-
return Object.fromEntries(names.map((name) => [name, false]));
|
|
246
|
-
}
|
|
247
|
-
if (value === true || value == null) return { ...defaults };
|
|
248
|
-
if (Array.isArray(value)) {
|
|
249
|
-
const requested = new Set(value);
|
|
250
|
-
return Object.fromEntries(names.map((name) => [name, requested.has(name)]));
|
|
251
|
-
}
|
|
252
|
-
if (typeof value !== 'object') return { ...defaults };
|
|
253
|
-
return Object.fromEntries(names.map((name) => [
|
|
254
|
-
name,
|
|
255
|
-
value[name] == null ? defaults[name] : Boolean(value[name]),
|
|
256
|
-
]));
|
|
257
|
-
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function createRuntimeSeed() {
|
|
2
|
+
const values = new Uint32Array(1);
|
|
3
|
+
try {
|
|
4
|
+
globalThis.crypto?.getRandomValues?.(values);
|
|
5
|
+
} catch {
|
|
6
|
+
values[0] = 0;
|
|
7
|
+
}
|
|
8
|
+
const highResolutionTime = globalThis.performance?.now?.() ?? 0;
|
|
9
|
+
return (values[0] || Math.floor(Date.now() + highResolutionTime * 1000)) >>> 0;
|
|
10
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { DEFAULT_PADDOCK_THEME_INPUT } from './theme/themeTokens.js';
|
|
2
|
+
import { DEFAULT_TELEMETRY_MODULES } from './telemetryModules.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_F1_SIMULATOR_OPTIONS = {
|
|
5
|
+
seed: 1971,
|
|
6
|
+
physicsMode: 'arcade',
|
|
7
|
+
warmup: {
|
|
8
|
+
enabled: true,
|
|
9
|
+
policy: 'config-change',
|
|
10
|
+
},
|
|
11
|
+
totalLaps: 10,
|
|
12
|
+
initialCameraMode: 'leader',
|
|
13
|
+
gridWalk: { enabled: false, initialView: 'grid-walk', ambientMotion: true },
|
|
14
|
+
title: 'F1 Simulator Lab',
|
|
15
|
+
kicker: 'Race Control',
|
|
16
|
+
backLinkHref: 'projects.html',
|
|
17
|
+
backLinkLabel: 'Projects',
|
|
18
|
+
showBackLink: true,
|
|
19
|
+
ui: {
|
|
20
|
+
layoutPreset: 'standard',
|
|
21
|
+
cameraControls: 'external',
|
|
22
|
+
viewControls: 'switch',
|
|
23
|
+
showFps: true,
|
|
24
|
+
showTimingTower: true,
|
|
25
|
+
showTelemetry: true,
|
|
26
|
+
telemetryIncludesOverview: true,
|
|
27
|
+
telemetryModules: { ...DEFAULT_TELEMETRY_MODULES },
|
|
28
|
+
showRaceDataPanel: true,
|
|
29
|
+
raceDataTelemetryDetail: false,
|
|
30
|
+
driverCamera: false,
|
|
31
|
+
raceDataBanners: {
|
|
32
|
+
initial: 'project',
|
|
33
|
+
enabled: ['project', 'radio'],
|
|
34
|
+
},
|
|
35
|
+
raceDataBannerSize: 'custom',
|
|
36
|
+
timingTowerVerticalFit: 'expand-race-view',
|
|
37
|
+
timingGapMode: 'interval',
|
|
38
|
+
timingGapModeToggle: true,
|
|
39
|
+
timingEntryVerticalPadding: 5,
|
|
40
|
+
responsiveNarrowLayout: true,
|
|
41
|
+
},
|
|
42
|
+
debug: {
|
|
43
|
+
physicsModeIndicator: false,
|
|
44
|
+
},
|
|
45
|
+
theme: DEFAULT_PADDOCK_THEME_INPUT,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const PADDOCK_SIMULATOR_PRESETS = {
|
|
49
|
+
dashboard: {
|
|
50
|
+
ui: {
|
|
51
|
+
layoutPreset: 'standard',
|
|
52
|
+
cameraControls: 'external',
|
|
53
|
+
showFps: true,
|
|
54
|
+
showTimingTower: true,
|
|
55
|
+
showTelemetry: true,
|
|
56
|
+
showRaceDataPanel: true,
|
|
57
|
+
raceDataBannerSize: 'custom',
|
|
58
|
+
timingTowerVerticalFit: 'expand-race-view',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
'timing-overlay': {
|
|
62
|
+
ui: {
|
|
63
|
+
layoutPreset: 'left-tower-overlay',
|
|
64
|
+
cameraControls: 'external',
|
|
65
|
+
showFps: false,
|
|
66
|
+
showTimingTower: true,
|
|
67
|
+
showTelemetry: true,
|
|
68
|
+
showRaceDataPanel: true,
|
|
69
|
+
raceDataBannerSize: 'auto',
|
|
70
|
+
timingTowerVerticalFit: 'expand-race-view',
|
|
71
|
+
raceDataBanners: {
|
|
72
|
+
initial: 'project',
|
|
73
|
+
enabled: ['project', 'radio'],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
'compact-race': {
|
|
78
|
+
ui: {
|
|
79
|
+
layoutPreset: 'standard',
|
|
80
|
+
cameraControls: 'external',
|
|
81
|
+
showFps: false,
|
|
82
|
+
showTimingTower: false,
|
|
83
|
+
showTelemetry: false,
|
|
84
|
+
showRaceDataPanel: true,
|
|
85
|
+
raceDataBannerSize: 'auto',
|
|
86
|
+
timingTowerVerticalFit: 'scroll',
|
|
87
|
+
},
|
|
88
|
+
theme: {
|
|
89
|
+
timingTowerMaxWidth: '340px',
|
|
90
|
+
raceViewMinHeight: '460px',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
'full-dashboard': {
|
|
94
|
+
ui: {
|
|
95
|
+
layoutPreset: 'standard',
|
|
96
|
+
cameraControls: 'external',
|
|
97
|
+
showFps: true,
|
|
98
|
+
showTimingTower: true,
|
|
99
|
+
showTelemetry: true,
|
|
100
|
+
telemetryIncludesOverview: true,
|
|
101
|
+
showRaceDataPanel: true,
|
|
102
|
+
raceDataBannerSize: 'custom',
|
|
103
|
+
timingTowerVerticalFit: 'expand-race-view',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const DEFAULT_TELEMETRY_MODULES = Object.freeze({
|
|
2
|
+
core: true,
|
|
3
|
+
sectors: true,
|
|
4
|
+
lapTimes: true,
|
|
5
|
+
sectorTimes: true,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export function normalizeTelemetryModules(value, defaults = DEFAULT_TELEMETRY_MODULES) {
|
|
9
|
+
const names = Object.keys(defaults);
|
|
10
|
+
if (value === false) return Object.fromEntries(names.map((name) => [name, false]));
|
|
11
|
+
if (value === true || value == null || typeof value !== 'object') return { ...defaults };
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
const requested = new Set(value);
|
|
14
|
+
return Object.fromEntries(names.map((name) => [name, requested.has(name)]));
|
|
15
|
+
}
|
|
16
|
+
return Object.fromEntries(names.map((name) => [
|
|
17
|
+
name,
|
|
18
|
+
value[name] == null ? defaults[name] : Boolean(value[name]),
|
|
19
|
+
]));
|
|
20
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const GENERATED_COLOR_CACHE = new Map();
|
|
2
|
+
|
|
3
|
+
export function normalizeCssColorToken(value, fallback = null) {
|
|
4
|
+
if (typeof value !== 'string') return fallback;
|
|
5
|
+
const candidate = value.trim();
|
|
6
|
+
if (
|
|
7
|
+
candidate === '' ||
|
|
8
|
+
/[\u0000-\u001F\u007F;]/.test(candidate) ||
|
|
9
|
+
/[{};]/.test(candidate) ||
|
|
10
|
+
/\b(?:url|expression)\s*\(/i.test(candidate)
|
|
11
|
+
) return fallback;
|
|
12
|
+
|
|
13
|
+
if (/^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(candidate)) return normalizeHexColor(candidate);
|
|
14
|
+
if (/^(?:rgb|rgba|hsl|hsla)\([0-9\s.,%/+*-]+\)$/i.test(candidate)) return candidate;
|
|
15
|
+
if (/^(?:transparent|currentcolor|black|white|red|green|blue|yellow|orange|purple|pink|gray|grey)$/i.test(candidate)) {
|
|
16
|
+
return candidate.toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeCssSizeToken(value, fallback = null) {
|
|
22
|
+
if (typeof value !== 'string' && typeof value !== 'number') return fallback;
|
|
23
|
+
const candidate = String(value).trim();
|
|
24
|
+
if (
|
|
25
|
+
candidate === '' ||
|
|
26
|
+
/[\u0000-\u001F\u007F;]/.test(candidate) ||
|
|
27
|
+
/[{};]/.test(candidate) ||
|
|
28
|
+
/\b(?:url|expression)\s*\(/i.test(candidate)
|
|
29
|
+
) return fallback;
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function deriveModeColor(mode, key, value) {
|
|
34
|
+
const cacheKey = `${mode}:${key}:${value}`;
|
|
35
|
+
if (GENERATED_COLOR_CACHE.has(cacheKey)) return GENERATED_COLOR_CACHE.get(cacheKey);
|
|
36
|
+
|
|
37
|
+
const rgb = parseHexColor(value);
|
|
38
|
+
const generated = rgb ? rgbToHex(adjustRgbForMode(rgb, mode)) : value;
|
|
39
|
+
GENERATED_COLOR_CACHE.set(cacheKey, generated);
|
|
40
|
+
return generated;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseHexColor(value) {
|
|
44
|
+
const normalized = normalizeCssColorToken(value, null);
|
|
45
|
+
if (!normalized?.startsWith?.('#')) return null;
|
|
46
|
+
const hex = normalized.slice(1);
|
|
47
|
+
const expanded = hex.length === 3
|
|
48
|
+
? hex.split('').map((char) => `${char}${char}`).join('')
|
|
49
|
+
: hex;
|
|
50
|
+
return {
|
|
51
|
+
r: Number.parseInt(expanded.slice(0, 2), 16),
|
|
52
|
+
g: Number.parseInt(expanded.slice(2, 4), 16),
|
|
53
|
+
b: Number.parseInt(expanded.slice(4, 6), 16),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeHexColor(value) {
|
|
58
|
+
const hex = value.slice(1);
|
|
59
|
+
if (hex.length === 3) {
|
|
60
|
+
return `#${hex.split('').map((char) => `${char}${char}`).join('')}`.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
return value.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function adjustRgbForMode(rgb, mode) {
|
|
66
|
+
const amount = mode === 'light' ? 0.18 : -0.18;
|
|
67
|
+
return {
|
|
68
|
+
r: adjustChannel(rgb.r, amount),
|
|
69
|
+
g: adjustChannel(rgb.g, amount),
|
|
70
|
+
b: adjustChannel(rgb.b, amount),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function adjustChannel(channel, amount) {
|
|
75
|
+
const target = amount > 0 ? 255 : 0;
|
|
76
|
+
return Math.round(channel + (target - channel) * Math.abs(amount));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function rgbToHex({ r, g, b }) {
|
|
80
|
+
return `#${[r, g, b].map((channel) => clampColorChannel(channel).toString(16).padStart(2, '0')).join('')}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clampColorChannel(value) {
|
|
84
|
+
return Math.max(0, Math.min(255, Math.round(value)));
|
|
85
|
+
}
|