@inventure71/paddockjs 4.2.0 → 4.3.1
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 +40 -0
- package/INSTALL_AND_UPDATE.md +24 -4
- package/README.md +31 -2
- package/docs/advanced_ai_pace.md +131 -0
- package/docs/advanced_physics_rebuild.md +166 -0
- package/docs/architecture.md +109 -30
- package/docs/cleanup_coverage.md +284 -0
- package/docs/component_inventory.md +31 -15
- 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/grid_walk.md +84 -0
- package/docs/index.md +1 -0
- package/docs/keyboard_driving_fix.md +118 -0
- package/docs/limit_driving_2026-10-14.md +88 -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 +38 -11
- package/docs/testing-reliability-guide.md +5 -0
- package/docs/v11_improvements.md +108 -0
- package/package.json +24 -13
- 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 +106 -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 +13 -259
- 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 +21 -140
- 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 +34 -259
- 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 +5 -11
- 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 +14 -3
- package/src/simulation/driver/edgeRecovery.js +0 -1
- package/src/simulation/driver/racingControls.js +10 -406
- package/src/simulation/driver/racingDriverProfile.js +48 -0
- package/src/simulation/driver/racingLinePlan.js +48 -15
- package/src/simulation/driver/rejoinControls.js +58 -111
- package/src/simulation/driver/safetyCarControls.js +12 -0
- package/src/simulation/driver/trafficScan.js +89 -6
- 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 +1 -1
- package/src/simulation/race/redFlag.js +2 -4
- package/src/simulation/race/safetyCar.js +3 -8
- 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/rulesReview.js +1 -7
- 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 +3 -3
- 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 +1 -25
- 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 +4 -241
- 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 +20 -6
- 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 +0 -56
- package/src/simulation/vehicle/vehicleKinematics.js +10 -0
- package/src/simulation/vehicle/vehiclePhysics.js +35 -194
- package/src/simulation/vehicle/vehicleSnapshots.js +13 -4
- package/src/simulation/vehicle/vehicleState.js +2 -1
- 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/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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export class HostLifecycleEmitter {
|
|
2
|
+
constructor(getOptions, getDispatchSource = () => undefined) {
|
|
3
|
+
this.getOptions = getOptions;
|
|
4
|
+
this.getDispatchSource = getDispatchSource;
|
|
5
|
+
this.reset();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
reset() {
|
|
9
|
+
this.dispatchGeneration = (this.dispatchGeneration ?? 0) + 1;
|
|
10
|
+
this.lastLeaderLap = null;
|
|
11
|
+
this.emittedRaceEventKeys = new Set();
|
|
12
|
+
this.raceFinishEmitted = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
captureDispatch() {
|
|
16
|
+
return { generation: this.dispatchGeneration, source: this.getDispatchSource() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isDispatchCurrent(dispatch) {
|
|
20
|
+
return dispatch.source !== false &&
|
|
21
|
+
dispatch.generation === this.dispatchGeneration &&
|
|
22
|
+
dispatch.source === this.getDispatchSource();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
emitCallback(name, ...args) {
|
|
26
|
+
const options = this.getOptions?.();
|
|
27
|
+
const callback = options?.[name];
|
|
28
|
+
if (typeof callback !== 'function') return;
|
|
29
|
+
try {
|
|
30
|
+
callback(...args);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (name !== 'onError' && typeof options?.onError === 'function') {
|
|
33
|
+
try {
|
|
34
|
+
options.onError(error, { callback: name });
|
|
35
|
+
} catch {
|
|
36
|
+
// Host callbacks should not break the simulator runtime.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
emitSnapshot(snapshot) {
|
|
43
|
+
const dispatch = this.captureDispatch();
|
|
44
|
+
if (!this.isDispatchCurrent(dispatch)) return;
|
|
45
|
+
const leader = snapshot.cars[0];
|
|
46
|
+
const leaderLap = leader?.lap;
|
|
47
|
+
if (Number.isFinite(leaderLap)) {
|
|
48
|
+
if (this.lastLeaderLap != null && leaderLap !== this.lastLeaderLap) {
|
|
49
|
+
this.emitCallback('onLapChange', {
|
|
50
|
+
previousLeaderLap: this.lastLeaderLap,
|
|
51
|
+
leaderLap,
|
|
52
|
+
leader,
|
|
53
|
+
snapshot,
|
|
54
|
+
});
|
|
55
|
+
if (!this.isDispatchCurrent(dispatch)) return;
|
|
56
|
+
}
|
|
57
|
+
this.lastLeaderLap = leaderLap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.emitRaceEvents(snapshot.events, snapshot, dispatch);
|
|
61
|
+
if (!this.isDispatchCurrent(dispatch)) return;
|
|
62
|
+
|
|
63
|
+
if (snapshot.raceControl.finished && !this.raceFinishEmitted) {
|
|
64
|
+
this.raceFinishEmitted = true;
|
|
65
|
+
this.emitCallback('onRaceFinish', {
|
|
66
|
+
winner: snapshot.raceControl.winner,
|
|
67
|
+
classification: snapshot.raceControl.classification ?? [],
|
|
68
|
+
snapshot,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
emitRaceEvents(events = [], snapshot, dispatch = this.captureDispatch()) {
|
|
74
|
+
for (const event of events) {
|
|
75
|
+
if (!this.isDispatchCurrent(dispatch)) return;
|
|
76
|
+
const key = [
|
|
77
|
+
event.type,
|
|
78
|
+
event.at ?? snapshot?.time,
|
|
79
|
+
event.id ?? '',
|
|
80
|
+
event.penaltyId ?? '',
|
|
81
|
+
event.driverId ?? '',
|
|
82
|
+
event.carId ?? '',
|
|
83
|
+
event.otherCarId ?? '',
|
|
84
|
+
event.winnerId ?? '',
|
|
85
|
+
event.sequence ?? '',
|
|
86
|
+
event.status ?? '',
|
|
87
|
+
].join(':');
|
|
88
|
+
if (this.emittedRaceEventKeys.has(key)) continue;
|
|
89
|
+
this.emittedRaceEventKeys.add(key);
|
|
90
|
+
this.emitCallback('onRaceEvent', event, snapshot);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -52,7 +52,7 @@ export function shouldAutoHideActiveRaceData({ activeRaceDataId, options, readou
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function renderRaceData({ car, drivers, readouts, options }) {
|
|
55
|
+
export function renderRaceData({ car, drivers, readouts, options, presentation = null }) {
|
|
56
56
|
const panels = getRaceDataPanelBindings(readouts);
|
|
57
57
|
if (!car || !panels.length) return false;
|
|
58
58
|
if (typeof options.isRaceDataBannerEnabled === 'function' && !options.isRaceDataBannerEnabled('project')) return false;
|
|
@@ -66,11 +66,16 @@ export function renderRaceData({ car, drivers, readouts, options }) {
|
|
|
66
66
|
binding.panel.classList.remove('is-radio-mode');
|
|
67
67
|
clearRaceDataIdleMode(binding.panel);
|
|
68
68
|
setText(binding.kicker, 'Project');
|
|
69
|
-
setText(binding.title, driver.name);
|
|
69
|
+
setText(binding.title, presentation?.name ?? driver.name);
|
|
70
70
|
setText(binding.number, formatDriverNumber(car.driverNumber ?? driver.driverNumber));
|
|
71
|
-
setText(binding.subtitle, `${car.code} - P${car.rank} - ${driver.raceData?.[0] ?? 'Project entry'}`);
|
|
71
|
+
setText(binding.subtitle, `${car.code} - P${car.rank} - ${presentation?.summary ?? driver.raceData?.[0] ?? 'Project entry'}`);
|
|
72
72
|
if (binding.open) {
|
|
73
|
-
binding.open.
|
|
73
|
+
if (binding.open.tagName === 'A') {
|
|
74
|
+
binding.open.hidden = !presentation?.href;
|
|
75
|
+
if (presentation?.href) binding.open.setAttribute('href', presentation.href);
|
|
76
|
+
else binding.open.removeAttribute('href');
|
|
77
|
+
setText(binding.open, presentation?.linkLabel ?? 'Open project');
|
|
78
|
+
} else binding.open.hidden = typeof options.onDriverOpen !== 'function';
|
|
74
79
|
}
|
|
75
80
|
});
|
|
76
81
|
return true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { metersToSimUnits } from '../../simulation/units.js';
|
|
2
|
-
import { VEHICLE_GEOMETRY } from '../../simulation/vehicleGeometry.js';
|
|
2
|
+
import { VEHICLE_GEOMETRY } from '../../simulation/vehicle/vehicleGeometry.js';
|
|
3
3
|
|
|
4
4
|
export const CAMERA_TARGET_LERP = 0.12;
|
|
5
5
|
export const CAMERA_SCALE_LERP = 0.12;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WORLD } from '../../simulation/trackModel.js';
|
|
1
|
+
import { WORLD } from '../../simulation/track/trackModel.js';
|
|
2
2
|
import { clamp, normalizeAngle } from '../../simulation/simMath.js';
|
|
3
3
|
import {
|
|
4
4
|
CAMERA_PRESETS,
|
|
@@ -274,14 +274,7 @@ export class CameraController {
|
|
|
274
274
|
return leader ? { x: leader.x, y: leader.y } : { x: WORLD.width / 2, y: WORLD.height / 2 };
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
|
|
278
|
-
const width = this.canvasHost.clientWidth || 900;
|
|
279
|
-
const height = this.canvasHost.clientHeight || 640;
|
|
280
|
-
const safeArea = this.getSafeArea(width);
|
|
281
|
-
return Math.min(safeArea.width / (WORLD.width + 260), height / (WORLD.height + 220));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
applyToWorldLayer(worldLayer, snapshot, { immediate = false, selectedId = null } = {}) {
|
|
277
|
+
applyToWorldLayer(worldLayer, snapshot, { immediate = false, selectedId = null, elapsedMs = 1000 / 60 } = {}) {
|
|
285
278
|
if (!worldLayer) return;
|
|
286
279
|
|
|
287
280
|
const width = this.canvasHost.clientWidth || 900;
|
|
@@ -293,18 +286,22 @@ export class CameraController {
|
|
|
293
286
|
const target = frame.target;
|
|
294
287
|
const targetRotation = Number.isFinite(frame.rotation) ? frame.rotation : 0;
|
|
295
288
|
const snapCamera = immediate || !this.camera.initialized;
|
|
289
|
+
const elapsedFrames = Math.max(0, elapsedMs) / (1000 / 60);
|
|
290
|
+
const targetLerp = 1 - (1 - CAMERA_TARGET_LERP) ** elapsedFrames;
|
|
291
|
+
const scaleLerp = 1 - (1 - CAMERA_SCALE_LERP) ** elapsedFrames;
|
|
292
|
+
const rotationLerp = 1 - (1 - CAMERA_ROTATION_LERP) ** elapsedFrames;
|
|
296
293
|
this.camera.x = snapCamera
|
|
297
294
|
? target.x
|
|
298
|
-
: this.camera.x + (target.x - this.camera.x) *
|
|
295
|
+
: this.camera.x + (target.x - this.camera.x) * targetLerp;
|
|
299
296
|
this.camera.y = snapCamera
|
|
300
297
|
? target.y
|
|
301
|
-
: this.camera.y + (target.y - this.camera.y) *
|
|
298
|
+
: this.camera.y + (target.y - this.camera.y) * targetLerp;
|
|
302
299
|
const activeScale = snapCamera || this.camera.scale === null
|
|
303
300
|
? scale
|
|
304
|
-
: this.camera.scale + (scale - this.camera.scale) *
|
|
301
|
+
: this.camera.scale + (scale - this.camera.scale) * scaleLerp;
|
|
305
302
|
const activeRotation = snapCamera
|
|
306
303
|
? targetRotation
|
|
307
|
-
: this.camera.rotation + normalizeAngle(targetRotation - this.camera.rotation) *
|
|
304
|
+
: this.camera.rotation + normalizeAngle(targetRotation - this.camera.rotation) * rotationLerp;
|
|
308
305
|
this.camera.initialized = true;
|
|
309
306
|
this.camera.scale = activeScale;
|
|
310
307
|
this.camera.rotation = activeRotation;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Separately mounted controls share one runtime view; they never own race state.
|
|
2
|
+
export class SimulatorViewControls {
|
|
3
|
+
constructor(root, onView, signal) {
|
|
4
|
+
this.surfaces = [...root.querySelectorAll('[data-paddock-component="view-controls"]')];
|
|
5
|
+
this.buttons = this.surfaces.flatMap((surface) => [...surface.querySelectorAll('[data-grid-view]')]);
|
|
6
|
+
this.toggles = this.surfaces.flatMap((surface) => [...surface.querySelectorAll('[data-grid-toggle]')]);
|
|
7
|
+
this.view = this.buttons.find((button) => button.getAttribute('aria-pressed') === 'true')?.dataset.gridView ?? 'race';
|
|
8
|
+
for (const button of this.buttons) {
|
|
9
|
+
button.addEventListener('click', () => onView(button.dataset.gridView), { signal });
|
|
10
|
+
button.disabled = false;
|
|
11
|
+
}
|
|
12
|
+
for (const button of this.toggles) {
|
|
13
|
+
button.addEventListener('click', () => onView(this.view === 'race' ? 'grid-walk' : 'race'), { signal });
|
|
14
|
+
button.disabled = false;
|
|
15
|
+
}
|
|
16
|
+
for (const surface of this.surfaces) surface.hidden = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
update(view, variant = 'switch') {
|
|
20
|
+
this.view = view;
|
|
21
|
+
const toggle = variant === 'toggle';
|
|
22
|
+
for (const button of this.buttons) {
|
|
23
|
+
button.hidden = toggle;
|
|
24
|
+
button.setAttribute('aria-pressed', String(button.dataset.gridView === view));
|
|
25
|
+
}
|
|
26
|
+
for (const button of this.toggles) {
|
|
27
|
+
button.hidden = !toggle;
|
|
28
|
+
button.dataset.gridAction = view === 'grid-walk' ? 'start' : 'stop';
|
|
29
|
+
const label = button.querySelector?.('[data-control-label]') ?? button;
|
|
30
|
+
label.textContent = view === 'grid-walk' ? 'Start race' : 'Stop race';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
focusRace() {
|
|
35
|
+
const button = [...this.buttons.filter((node) => node.dataset.gridView === 'race'), ...this.toggles]
|
|
36
|
+
.find((node) => !node.hidden && node.checkVisibility());
|
|
37
|
+
button?.focus({ preventScroll: true });
|
|
38
|
+
return Boolean(button);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
destroy() {
|
|
42
|
+
for (const button of [...this.buttons, ...this.toggles]) button.disabled = true;
|
|
43
|
+
for (const surface of this.surfaces) surface.hidden = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
function uniqueNodes(nodes = []) {
|
|
2
|
+
return [...new Set([...nodes].filter(Boolean))];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// Bind DOM gestures to commands without owning race state or a simulation.
|
|
6
|
+
export function bindWorkbenchControls(nodes, commands, signal) {
|
|
7
|
+
const eventOptions = { signal };
|
|
8
|
+
nodes.safetyButtons.forEach((button) => {
|
|
9
|
+
button.addEventListener('click', commands.toggleSafetyCar, eventOptions);
|
|
10
|
+
});
|
|
11
|
+
uniqueNodes(nodes.restartButtons?.length ? nodes.restartButtons : [nodes.restartButton]).forEach((button) => button.addEventListener('click', commands.restart, eventOptions));
|
|
12
|
+
nodes.cameraButtons.forEach((button) => {
|
|
13
|
+
button.addEventListener('click', () => commands.setCameraMode(button.dataset.cameraMode), eventOptions);
|
|
14
|
+
});
|
|
15
|
+
for (const [buttons, fallback, direction] of [[nodes.zoomInButtons, nodes.zoomInButton, 1], [nodes.zoomOutButtons, nodes.zoomOutButton, -1]]) {
|
|
16
|
+
uniqueNodes(buttons?.length ? buttons : [fallback]).forEach((button) => button.addEventListener('click', () => commands.adjustCameraZoom(direction), eventOptions));
|
|
17
|
+
}
|
|
18
|
+
nodes.simulationSpeedButtons?.forEach((button) => {
|
|
19
|
+
button.addEventListener('click', commands.cycleSimulationSpeed, eventOptions);
|
|
20
|
+
});
|
|
21
|
+
commands.updateSimulationSpeedControls();
|
|
22
|
+
nodes.bannerMuteButtons?.forEach((button) => {
|
|
23
|
+
button.addEventListener('click', commands.toggleRaceDataBanners, eventOptions);
|
|
24
|
+
});
|
|
25
|
+
nodes.overviewModeButtons.forEach((button) => {
|
|
26
|
+
button.addEventListener('click', () => {
|
|
27
|
+
commands.setOverviewMode(button.dataset.overviewMode === 'driver' ? 'driver' : 'vehicle');
|
|
28
|
+
}, eventOptions);
|
|
29
|
+
});
|
|
30
|
+
nodes.timingGapModeButtons.forEach((button) => {
|
|
31
|
+
button.addEventListener('click', commands.toggleTimingGapMode, eventOptions);
|
|
32
|
+
});
|
|
33
|
+
nodes.timingLists?.forEach?.((timingList) => {
|
|
34
|
+
timingList.addEventListener?.('click', (event) => {
|
|
35
|
+
const row = event.target instanceof Element ? event.target.closest('[data-driver-id]') : null;
|
|
36
|
+
if (row) commands.selectCar(row.dataset.driverId);
|
|
37
|
+
}, eventOptions);
|
|
38
|
+
});
|
|
39
|
+
nodes.timingPanelToggles?.forEach?.((toggle) => {
|
|
40
|
+
toggle.addEventListener?.('click', () => {
|
|
41
|
+
const racePanel = toggle.closest?.('.sim-canvas-panel');
|
|
42
|
+
commands.setTimingPanelOpen(!racePanel?.classList?.contains?.('is-timing-panel-open'), racePanel);
|
|
43
|
+
}, eventOptions);
|
|
44
|
+
});
|
|
45
|
+
const openButtons = uniqueNodes(nodes.raceDataOpens?.length ? nodes.raceDataOpens : [nodes.openButton]);
|
|
46
|
+
openButtons.forEach((button) => button.addEventListener('click', commands.openDriver, eventOptions));
|
|
47
|
+
const dismissButtons = uniqueNodes(nodes.raceDataDismisses?.length ? nodes.raceDataDismisses : [nodes.raceDataDismiss]);
|
|
48
|
+
dismissButtons.forEach((button) => button.addEventListener('click', (event) => {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
event.stopPropagation();
|
|
51
|
+
commands.dismissRaceDataPanel(performance.now());
|
|
52
|
+
}, eventOptions));
|
|
53
|
+
nodes.telemetryDrawerToggle?.addEventListener('click', commands.toggleTelemetryDrawer, eventOptions);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function bindCameraWheelControls(canvasHost, adjustCameraZoom, eventOptions, isEnabled = () => true) {
|
|
57
|
+
if (!canvasHost?.addEventListener) return;
|
|
58
|
+
canvasHost.addEventListener('wheel', (event) => {
|
|
59
|
+
if (!isEnabled() || !event.cancelable || event.defaultPrevented || event.ctrlKey || event.metaKey ||
|
|
60
|
+
!Number.isFinite(event.deltaY) || event.deltaY === 0) return;
|
|
61
|
+
if (!adjustCameraZoom(event.deltaY > 0 ? -1 : 1)) return;
|
|
62
|
+
event.preventDefault?.();
|
|
63
|
+
}, { ...eventOptions, passive: false });
|
|
64
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function renderTelemetryDrawerDisclosure({ workbench, drawer, toggle }, open) {
|
|
2
|
+
workbench?.classList?.toggle?.('is-telemetry-open', open);
|
|
3
|
+
if (open) {
|
|
4
|
+
drawer?.removeAttribute?.('inert');
|
|
5
|
+
drawer?.setAttribute?.('aria-hidden', 'false');
|
|
6
|
+
} else {
|
|
7
|
+
drawer?.setAttribute?.('inert', '');
|
|
8
|
+
drawer?.setAttribute?.('aria-hidden', 'true');
|
|
9
|
+
}
|
|
10
|
+
toggle?.setAttribute?.('aria-expanded', String(open));
|
|
11
|
+
if (toggle) {
|
|
12
|
+
const label = toggle.querySelector?.('[data-control-label]') ?? toggle;
|
|
13
|
+
label.textContent = open ? 'Close' : 'Telemetry';
|
|
14
|
+
toggle.setAttribute('aria-label', open ? 'Close telemetry' : 'Open telemetry');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isTimingPanelDisclosureMode(toggle) {
|
|
19
|
+
if (!toggle) return false;
|
|
20
|
+
const style = getComputedStyle(toggle);
|
|
21
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function syncTimingPanelDisclosureState({ toggles, targetPanel = null, isDisclosureMode = isTimingPanelDisclosureMode }) {
|
|
25
|
+
for (const toggle of [...(toggles ?? [])]) {
|
|
26
|
+
const racePanel = toggle.closest?.('.sim-canvas-panel');
|
|
27
|
+
if (targetPanel && racePanel !== targetPanel) continue;
|
|
28
|
+
const tower = racePanel?.querySelector?.('[data-timing-tower]');
|
|
29
|
+
const disclosureMode = isDisclosureMode(toggle);
|
|
30
|
+
const open = disclosureMode && Boolean(racePanel?.classList?.contains?.('is-timing-panel-open'));
|
|
31
|
+
racePanel?.classList?.toggle?.('is-timing-panel-open', open);
|
|
32
|
+
if (!disclosureMode || open) {
|
|
33
|
+
tower?.removeAttribute?.('inert');
|
|
34
|
+
tower?.setAttribute?.('aria-hidden', 'false');
|
|
35
|
+
} else {
|
|
36
|
+
tower?.setAttribute?.('inert', '');
|
|
37
|
+
tower?.setAttribute?.('aria-hidden', 'true');
|
|
38
|
+
}
|
|
39
|
+
toggle.setAttribute?.('aria-expanded', String(open));
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/app/domBindings.js
CHANGED
|
@@ -13,7 +13,7 @@ export function resolveNodes(nodes, fallbackNode = null) {
|
|
|
13
13
|
return resolved;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function queryRaceDataPanelBindings(root) {
|
|
16
|
+
export function queryRaceDataPanelBindings(root) {
|
|
17
17
|
const panelSelector = '[data-race-data-panel]';
|
|
18
18
|
const panels = toArray(root.querySelectorAll(panelSelector));
|
|
19
19
|
const fallbackPanel = panels.length ? null : root.querySelector(panelSelector);
|
|
@@ -36,11 +36,6 @@ function queryRaceDataPanelBindings(root) {
|
|
|
36
36
|
const fallbackDismiss = first.dismiss ?? root.querySelector('[data-race-data-dismiss]');
|
|
37
37
|
return {
|
|
38
38
|
raceDataPanelBindings: bindings,
|
|
39
|
-
raceDataPanels,
|
|
40
|
-
raceDataKickers: bindings.map((binding) => binding.kicker).filter(Boolean),
|
|
41
|
-
raceDataTitles: bindings.map((binding) => binding.title).filter(Boolean),
|
|
42
|
-
raceDataNumbers: bindings.map((binding) => binding.number).filter(Boolean),
|
|
43
|
-
raceDataSubtitles: bindings.map((binding) => binding.subtitle).filter(Boolean),
|
|
44
39
|
raceDataOpens: bindings.map((binding) => binding.open).filter(Boolean),
|
|
45
40
|
raceDataDismisses: bindings.map((binding) => binding.dismiss).filter(Boolean),
|
|
46
41
|
raceDataPanel: first.panel ?? null,
|
|
@@ -53,18 +48,27 @@ function queryRaceDataPanelBindings(root) {
|
|
|
53
48
|
};
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
export function queryCarOverviewBindings(root) {
|
|
52
|
+
return toArray(root.querySelectorAll('[data-paddock-component="car-driver-overview"]')).map((panel) => ({
|
|
53
|
+
carOverview: panel,
|
|
54
|
+
...Object.fromEntries([
|
|
55
|
+
['carOverviewTitle', '[data-car-overview-title]'], ['carOverviewDiagram', '.car-overview-diagram'],
|
|
56
|
+
['carOverviewCode', '[data-car-overview-code]'], ['carOverviewIcon', '[data-car-overview-icon]'],
|
|
57
|
+
['carOverviewImage', '[data-car-overview-image]'], ['carOverviewNumber', '[data-car-overview-number]'],
|
|
58
|
+
['carOverviewCoreStat', '[data-car-overview-core-stat]'],
|
|
59
|
+
].map(([name, selector]) => [name, panel.querySelector(selector)])),
|
|
60
|
+
carOverviewFieldBindings: toArray(panel.querySelectorAll('[data-overview-field]')).map((field) => ({
|
|
61
|
+
field, label: field.querySelector('[data-overview-field-label]'), value: field.querySelector('[data-overview-field-value]'),
|
|
62
|
+
})),
|
|
62
63
|
}));
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export function querySimulatorDom(root) {
|
|
66
67
|
const raceDataBindings = queryRaceDataPanelBindings(root);
|
|
67
|
-
const
|
|
68
|
+
const carOverviewBindings = queryCarOverviewBindings(root);
|
|
69
|
+
const carOverviewFieldBindings = carOverviewBindings[0]?.carOverviewFieldBindings ?? toArray(root.querySelectorAll('[data-overview-field]')).map((field) => ({
|
|
70
|
+
field, label: field.querySelector?.('[data-overview-field-label]') ?? null, value: field.querySelector?.('[data-overview-field-value]') ?? null,
|
|
71
|
+
}));
|
|
68
72
|
const readouts = {
|
|
69
73
|
timingTower: root.querySelector('[data-timing-tower]'),
|
|
70
74
|
timingTowers: root.querySelectorAll('[data-timing-tower]'),
|
|
@@ -123,6 +127,7 @@ export function querySimulatorDom(root) {
|
|
|
123
127
|
carOverviewNumber: root.querySelector('[data-car-overview-number]'),
|
|
124
128
|
carOverviewCoreStat: root.querySelector('[data-car-overview-core-stat]'),
|
|
125
129
|
carOverviewFields: root.querySelectorAll('[data-overview-field]'),
|
|
130
|
+
carOverviewBindings,
|
|
126
131
|
carOverviewFieldBindings,
|
|
127
132
|
telemetryDrawerWorkbench: root.querySelector('[data-race-telemetry-drawer]'),
|
|
128
133
|
telemetryDrawer: root.querySelector('[data-telemetry-drawer]'),
|
|
@@ -141,6 +146,7 @@ export function querySimulatorDom(root) {
|
|
|
141
146
|
canvasHost: root.querySelector('[data-track-canvas]'),
|
|
142
147
|
safetyButtons: root.querySelectorAll('[data-safety-car]'),
|
|
143
148
|
restartButton: root.querySelector('[data-restart-race]'),
|
|
149
|
+
restartButtons: root.querySelectorAll('[data-restart-race]'),
|
|
144
150
|
openButton: raceDataBindings.raceDataOpen,
|
|
145
151
|
timingList: root.querySelector('[data-timing-list]'),
|
|
146
152
|
timingGapModeButtons: root.querySelectorAll('[data-timing-gap-toggle]'),
|
|
@@ -149,7 +155,9 @@ export function querySimulatorDom(root) {
|
|
|
149
155
|
bannerMuteButtons: root.querySelectorAll('[data-race-data-banners-muted]'),
|
|
150
156
|
simulationSpeedButtons: root.querySelectorAll('[data-simulation-speed]'),
|
|
151
157
|
zoomInButton: root.querySelector('[data-zoom-in]'),
|
|
158
|
+
zoomInButtons: root.querySelectorAll('[data-zoom-in]'),
|
|
152
159
|
zoomOutButton: root.querySelector('[data-zoom-out]'),
|
|
160
|
+
zoomOutButtons: root.querySelectorAll('[data-zoom-out]'),
|
|
153
161
|
timingLists: root.querySelectorAll('[data-timing-list]'),
|
|
154
162
|
readouts,
|
|
155
163
|
startLightNodes: [...(readouts.startLights?.querySelectorAll('.start-lights__gantry span') ?? [])],
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createGridWalkMarkup } from '../../ui/gridWalkTemplate.js';
|
|
2
|
+
import { GridWalkSession } from './gridWalkSession.js';
|
|
3
|
+
import { GridWalkCamera, gridWalkOrientation } from './gridWalkCamera.js';
|
|
4
|
+
import { GridWalkCrew } from './gridWalkCrew.js';
|
|
5
|
+
import { GridWalkNavigation } from './gridWalkNavigation.js';
|
|
6
|
+
import { GridWalkControls } from './gridWalkControls.js';
|
|
7
|
+
import { GridWalkLabels } from './gridWalkLabels.js';
|
|
8
|
+
import { GridWalkProjectBanner } from './gridWalkProjectBanner.js';
|
|
9
|
+
import { GridWalkLayout } from './gridWalkLayout.js';
|
|
10
|
+
import { SimulatorViewControls } from '../controls/SimulatorViewControls.js';
|
|
11
|
+
|
|
12
|
+
// Coordinates lifecycle and scene presentation. Each UI/input component owns
|
|
13
|
+
// its own actions and geometry; none owns or steps the simulation.
|
|
14
|
+
export class GridWalkController {
|
|
15
|
+
constructor(app) {
|
|
16
|
+
this.app = app;
|
|
17
|
+
this.session = new GridWalkSession(app.sim, app.options.gridWalk);
|
|
18
|
+
this.abort = new AbortController();
|
|
19
|
+
this.elapsed = 0;
|
|
20
|
+
this.lastTime = performance.now();
|
|
21
|
+
this.panel = app.canvasHost.closest('.sim-canvas-panel');
|
|
22
|
+
const wrapper = app.canvasHost.ownerDocument.createElement('div');
|
|
23
|
+
const cars = this.session.snapshot.cars;
|
|
24
|
+
wrapper.innerHTML = createGridWalkMarkup(app.drivers, cars, app.options.ui);
|
|
25
|
+
this.element = wrapper.firstElementChild;
|
|
26
|
+
const select = (id) => app.selectCar(id, { focus: true });
|
|
27
|
+
const { signal } = this.abort;
|
|
28
|
+
this.viewControls = new SimulatorViewControls(app.root, (view) => this.setView(view), signal);
|
|
29
|
+
this.controls = new GridWalkControls(this.element, {
|
|
30
|
+
onSelect: select, onMove: (direction) => this.move(direction),
|
|
31
|
+
}, signal);
|
|
32
|
+
this.labels = new GridWalkLabels(this.element, cars, select, signal);
|
|
33
|
+
this.banner = new GridWalkProjectBanner(this.element, () => this.closeProjectBanner(), signal);
|
|
34
|
+
this.camera = new GridWalkCamera(cars);
|
|
35
|
+
this.layout = new GridWalkLayout({ element: this.element, panel: this.panel, canvasHost: app.canvasHost, root: app.root });
|
|
36
|
+
this.navigation = new GridWalkNavigation({ host: app.canvasHost, surface: this.panel,
|
|
37
|
+
isEnabled: () => this.active && !this.session.clearing,
|
|
38
|
+
getOrientation: () => this.orientation,
|
|
39
|
+
onPan: (delta) => {
|
|
40
|
+
if (!this.camera.pan(delta)) return false;
|
|
41
|
+
this.render();
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
onHome: () => { this.camera.focus(app.selectedId); this.render(); }, onSelect: select,
|
|
45
|
+
}, signal);
|
|
46
|
+
this.media = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)');
|
|
47
|
+
this.media?.addEventListener('change', () => {
|
|
48
|
+
this.session.advance(0, this.motion);
|
|
49
|
+
this.sync();
|
|
50
|
+
}, { signal });
|
|
51
|
+
this.crew = new GridWalkCrew(app.worldLayer, cars);
|
|
52
|
+
this.panel.appendChild(this.element);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get active() { return this.session.active; }
|
|
56
|
+
get motion() { return this.app.options.gridWalk.ambientMotion && !this.media?.matches; }
|
|
57
|
+
get needsAnimation() { return this.active && this.motion; }
|
|
58
|
+
|
|
59
|
+
setView(view) {
|
|
60
|
+
if (!this.session.setView(view, this.motion)) return false;
|
|
61
|
+
this.navigation.cancel();
|
|
62
|
+
// Preserve the fixed-step remainder; only discard time spent browsing.
|
|
63
|
+
this.app.lastTime = this.lastTime = performance.now();
|
|
64
|
+
if (view === 'race' && this.element.contains(this.element.ownerDocument.activeElement) && !this.viewControls.focusRace()) {
|
|
65
|
+
this.app.canvasHost.querySelector('canvas').focus({ preventScroll: true });
|
|
66
|
+
}
|
|
67
|
+
this.sync();
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
select(id, { focus = false } = {}) {
|
|
72
|
+
if (!this.active || this.session.clearing || !this.app.driverById.has(id)) return;
|
|
73
|
+
// A visible car stays where it was clicked. The selector can bring an
|
|
74
|
+
// offscreen entry into view without resetting the camera on every click.
|
|
75
|
+
if (!this.labels.isVisible(id)) this.camera.focus(id);
|
|
76
|
+
this.banner.open = true;
|
|
77
|
+
this.sync();
|
|
78
|
+
if (focus) {
|
|
79
|
+
if (!this.banner.element.hidden) this.banner.focus();
|
|
80
|
+
else this.labels.focus(id, this.controls.jump);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
closeProjectBanner() {
|
|
85
|
+
this.banner.open = false;
|
|
86
|
+
this.sync();
|
|
87
|
+
this.labels.focus(this.app.selectedId, this.controls.jump);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
move(direction) {
|
|
91
|
+
const cars = this.session.snapshot.cars;
|
|
92
|
+
const index = cars.findIndex((car) => car.id === this.app.selectedId);
|
|
93
|
+
this.app.selectCar(cars[(index + direction + cars.length) % cars.length].id, { focus: true });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
sync({ updateTicker = true } = {}) {
|
|
97
|
+
const { app, session } = this;
|
|
98
|
+
this.navigation.sync();
|
|
99
|
+
this.panel.dataset.gridWalkActive = String(this.active);
|
|
100
|
+
this.element.dataset.view = session.view;
|
|
101
|
+
this.controls.update(session, app.selectedId);
|
|
102
|
+
this.viewControls.update(session.view, app.options.ui.viewControls);
|
|
103
|
+
this.element.querySelector('[data-grid-labels]').hidden = !this.active || session.clearing;
|
|
104
|
+
this.banner.update({ active: this.active, clearing: session.clearing,
|
|
105
|
+
enabled: app.isRaceDataBannerEnabled('project'), car: session.snapshot.cars.find((car) => car.id === app.selectedId),
|
|
106
|
+
drivers: app.drivers, options: app.options, presentation: app.driverById.get(app.selectedId).presentation });
|
|
107
|
+
for (const layer of [app.trailLayer, app.sensorLayer, app.pitLaneStatusLayer]) if (layer) layer.visible = !this.active;
|
|
108
|
+
this.syncLayout();
|
|
109
|
+
if (!this.active) app.syncTimingPanelDisclosureState();
|
|
110
|
+
if (this.active) this.render();
|
|
111
|
+
else {
|
|
112
|
+
this.crew.container.visible = false;
|
|
113
|
+
app.syncRendererToCurrentLayout({ render: true });
|
|
114
|
+
}
|
|
115
|
+
if (updateTicker) app.syncRuntimeTicker();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
syncLayout() { this.layout.update(this.active); }
|
|
119
|
+
|
|
120
|
+
applyCamera() {
|
|
121
|
+
const { app } = this;
|
|
122
|
+
this.syncLayout();
|
|
123
|
+
const width = app.canvasHost.clientWidth;
|
|
124
|
+
const height = app.canvasHost.clientHeight;
|
|
125
|
+
if (!(width > 0 && height > 0)) return;
|
|
126
|
+
const orientation = gridWalkOrientation(width, height, this.orientation);
|
|
127
|
+
if (this.orientation && orientation !== this.orientation) this.navigation.cancel();
|
|
128
|
+
this.orientation = orientation;
|
|
129
|
+
this.element.dataset.orientation = this.orientation;
|
|
130
|
+
this.element.dataset.compactControls = String(width < 600 && height < 420);
|
|
131
|
+
const area = this.layout.measure(this.banner.measure(width, height));
|
|
132
|
+
this.element.dataset.compactLabels = String(area.bottom - area.top < 164);
|
|
133
|
+
this.frame = this.camera.update({ selectedId: app.selectedId, width, height, orientation: this.orientation, area });
|
|
134
|
+
this.crew.updateCamera(this.frame, width, height);
|
|
135
|
+
app.worldLayer.rotation = this.frame.rotation;
|
|
136
|
+
app.worldLayer.scale.set(this.frame.scale);
|
|
137
|
+
app.worldLayer.position.set(this.frame.x, this.frame.y);
|
|
138
|
+
this.labels.update(this.frame, app.selectedId, this.controls.jump);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
render() {
|
|
142
|
+
const { app, session } = this;
|
|
143
|
+
const screen = app.app?.renderer?.screen;
|
|
144
|
+
if (!screen || screen.width !== Math.max(1, Math.round(app.canvasHost.clientWidth)) ||
|
|
145
|
+
screen.height !== Math.max(1, Math.round(app.canvasHost.clientHeight))) app.resizeRendererToCanvasHost();
|
|
146
|
+
this.applyCamera();
|
|
147
|
+
app.renderCars(session.snapshot);
|
|
148
|
+
this.crew.render({ active: this.active, motion: this.motion, elapsed: this.elapsed,
|
|
149
|
+
clearing: session.clearing, clearProgress: Math.min(1, session.clearElapsed / 650) });
|
|
150
|
+
app.app?.render?.();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
tick(now) {
|
|
154
|
+
if (!this.active) return false;
|
|
155
|
+
const elapsed = Math.max(0, Math.min(now - this.lastTime, 100));
|
|
156
|
+
this.lastTime = now;
|
|
157
|
+
this.elapsed += elapsed;
|
|
158
|
+
const wasClearing = this.session.clearing;
|
|
159
|
+
this.session.advance(elapsed, this.motion);
|
|
160
|
+
if (wasClearing && !this.session.clearing) this.sync();
|
|
161
|
+
if (!this.active) return false;
|
|
162
|
+
// A busy browser can defer the media callback; stop on this frame too.
|
|
163
|
+
if (!this.motion) this.app.syncRuntimeTicker();
|
|
164
|
+
this.crew.render({ active: true, motion: this.motion, elapsed: this.elapsed,
|
|
165
|
+
clearing: this.session.clearing, clearProgress: Math.min(1, this.session.clearElapsed / 650) });
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
destroy() {
|
|
170
|
+
this.navigation.destroy();
|
|
171
|
+
this.viewControls.destroy();
|
|
172
|
+
this.abort.abort();
|
|
173
|
+
try { this.crew.destroy(); } finally {
|
|
174
|
+
this.layout.destroy();
|
|
175
|
+
this.element.remove();
|
|
176
|
+
delete this.panel.dataset.gridWalkActive;
|
|
177
|
+
for (const layer of [this.app.trailLayer, this.app.sensorLayer, this.app.pitLaneStatusLayer]) if (layer) layer.visible = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|