@inventure71/paddockjs 2.0.1 → 3.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 +31 -1
- package/INSTALL_AND_UPDATE.md +10 -1
- package/README.md +65 -15
- package/docs/architecture.md +17 -13
- package/docs/component_inventory.md +72 -0
- package/docs/concepts.md +2 -2
- package/docs/custom_model_controller.md +2 -2
- package/docs/data_contract.md +96 -21
- package/docs/index.md +2 -1
- package/docs/learnings.md +6 -6
- package/docs/rules.md +7 -7
- package/docs/sense_contract.md +1 -1
- package/docs/system_specs.md +25 -18
- package/docs/training.md +13 -12
- package/package.json +1 -1
- package/src/api/PaddockSimulatorController.js +74 -4
- package/src/app/F1SimulatorApp.js +191 -28
- package/src/app/banners/raceDataBannerController.js +71 -36
- package/src/app/banners/stewardMessageController.js +5 -4
- package/src/app/camera/cameraConstants.js +2 -0
- package/src/app/camera/cameraController.js +86 -29
- package/src/app/camera/cameraGeometry.js +12 -1
- package/src/app/domBindings.js +89 -9
- package/src/app/layoutScheduler.js +27 -0
- package/src/app/layoutSupport.js +165 -0
- package/src/app/raceOverlayClearanceSupport.js +158 -0
- package/src/app/readouts/carOverviewRenderer.js +16 -8
- package/src/app/readouts/controlStateRenderer.js +1 -1
- package/src/app/readouts/raceStatusRenderer.js +30 -21
- package/src/app/readouts/readoutFormatters.js +2 -0
- package/src/app/readouts/telemetryRenderer.js +17 -8
- package/src/app/readouts/timingTowerRenderer.js +62 -10
- package/src/app/runtime/runtimeVisibility.js +66 -5
- package/src/config/cssValues.js +16 -0
- package/src/config/defaultOptions.js +51 -38
- package/src/config/themeOptions.js +825 -0
- package/src/config/timingGapMode.js +13 -0
- package/src/config/urlOptions.js +25 -0
- package/src/data/championship.js +14 -9
- package/src/environment/controllerLoop.js +17 -3
- package/src/environment/index.d.ts +1 -2
- package/src/environment/options.js +1 -1
- package/src/environment/runtime.js +3 -11
- package/src/environment/scenarios.js +1 -1
- package/src/environment/sensors/surfaceRays.js +41 -33
- package/src/environment/sensors/trackRays.js +44 -49
- package/src/index.d.ts +110 -9
- package/src/index.js +25 -1
- package/src/simulation/driver/driverController.js +1 -1
- package/src/simulation/driver/edgeRecovery.js +1 -1
- package/src/simulation/driver/racingControls.js +2 -2
- package/src/simulation/driver/rejoinControls.js +2 -2
- package/src/simulation/pit/pitKinematics.js +2 -2
- package/src/simulation/pit/pitRouting.js +2 -4
- package/src/simulation/race/raceSetup.js +2 -6
- package/src/simulation/race/raceStep.js +1 -1
- package/src/simulation/race/safetyCar.js +1 -1
- package/src/simulation/race/stalledDnf.js +1 -1
- package/src/simulation/timing/lapTelemetry.js +28 -9
- package/src/simulation/track/pitLaneLayout.js +7 -3
- package/src/simulation/track/spatialQueries.js +5 -28
- package/src/simulation/track/trackQueryIndex.js +180 -135
- package/src/simulation/track/trackStatePolicy.js +0 -1
- package/src/simulation/vehicle/contactResolution.js +3 -3
- package/src/simulation/vehicle/vehicleKinematics.js +2 -2
- package/src/simulation/vehicle/vehiclePhysics.js +3 -3
- package/src/simulation/vehicle/vehicleSnapshots.js +12 -1
- package/src/simulation/vehicle/vehicleState.js +1 -1
- package/src/simulation/warmup/runtimeWarmup.js +0 -1
- package/src/styles.css +1185 -136
- package/src/ui/bannerTemplates.js +38 -28
- package/src/ui/cameraControlsTemplate.js +16 -5
- package/src/ui/carOverviewTemplate.js +12 -5
- package/src/ui/raceCanvasTemplate.js +31 -8
- package/src/ui/raceControlsTemplate.js +27 -10
- package/src/ui/shellTemplate.js +3 -1
- package/src/ui/telemetryDrawerTemplate.js +23 -10
- package/src/ui/telemetryTemplates.js +58 -32
- package/src/ui/templateUtils.js +48 -0
- package/src/ui/timingTowerTemplate.js +29 -12
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { WORLD } from '../../simulation/trackModel.js';
|
|
2
|
-
import { clamp } from '../../simulation/simMath.js';
|
|
2
|
+
import { clamp, normalizeAngle } from '../../simulation/simMath.js';
|
|
3
3
|
import {
|
|
4
4
|
CAMERA_PRESETS,
|
|
5
|
+
CAMERA_ROTATION_LERP,
|
|
5
6
|
CAMERA_SCALE_LERP,
|
|
6
7
|
CAMERA_TARGET_LERP,
|
|
7
8
|
CAMERA_ZOOM_STEP,
|
|
@@ -27,16 +28,47 @@ export function createCameraState(initialCameraMode) {
|
|
|
27
28
|
initialized: false,
|
|
28
29
|
x: WORLD.width / 2,
|
|
29
30
|
y: WORLD.height / 2,
|
|
31
|
+
rotation: 0,
|
|
30
32
|
free: false,
|
|
31
33
|
freeTarget: null,
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
function timingTowerCandidates(readouts) {
|
|
38
|
+
const nodes = [];
|
|
39
|
+
readouts?.timingTowers?.forEach?.((node) => {
|
|
40
|
+
if (node && !nodes.includes(node)) nodes.push(node);
|
|
41
|
+
});
|
|
42
|
+
if (readouts?.timingTower && !nodes.includes(readouts.timingTower)) {
|
|
43
|
+
nodes.push(readouts.timingTower);
|
|
44
|
+
}
|
|
45
|
+
return nodes;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function timingTowerFrameForCanvas(readouts, canvasRect, canvasWidth) {
|
|
49
|
+
for (const timingTower of timingTowerCandidates(readouts)) {
|
|
50
|
+
const towerRect = timingTower?.getBoundingClientRect?.();
|
|
51
|
+
if (!towerRect) continue;
|
|
52
|
+
const overlapsCanvasHorizontally = towerRect.right > canvasRect.left && towerRect.left < canvasRect.right;
|
|
53
|
+
const hasVerticalBounds = Number.isFinite(canvasRect.top) &&
|
|
54
|
+
Number.isFinite(canvasRect.bottom) &&
|
|
55
|
+
Number.isFinite(towerRect.top) &&
|
|
56
|
+
Number.isFinite(towerRect.bottom);
|
|
57
|
+
const overlapsCanvasVertically = !hasVerticalBounds ||
|
|
58
|
+
(towerRect.bottom > canvasRect.top && towerRect.top < canvasRect.bottom);
|
|
59
|
+
const towerWidth = Math.max(0, towerRect.right - towerRect.left);
|
|
60
|
+
const isSideGutter = towerWidth < canvasWidth * 0.6;
|
|
61
|
+
if (overlapsCanvasHorizontally && overlapsCanvasVertically && isSideGutter) return towerRect;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
35
66
|
export class CameraController {
|
|
36
|
-
constructor({ canvasHost, readouts, initialMode }) {
|
|
67
|
+
constructor({ canvasHost, readouts, initialMode, driverCamera = false }) {
|
|
37
68
|
this.canvasHost = canvasHost;
|
|
38
69
|
this.readouts = readouts;
|
|
39
70
|
this.camera = createCameraState(initialMode);
|
|
71
|
+
this.driverCamera = Boolean(driverCamera);
|
|
40
72
|
this.safeAreaCache = null;
|
|
41
73
|
this.pitBoundsCache = null;
|
|
42
74
|
this.trackBoundsCache = null;
|
|
@@ -72,23 +104,14 @@ export class CameraController {
|
|
|
72
104
|
}
|
|
73
105
|
|
|
74
106
|
const canvasRect = this.canvasHost?.getBoundingClientRect?.();
|
|
75
|
-
|
|
76
|
-
if (!canvasRect || !towerRect) {
|
|
107
|
+
if (!canvasRect) {
|
|
77
108
|
const safeArea = { left: 0, width };
|
|
78
109
|
this.safeAreaCache = { width, safeArea };
|
|
79
110
|
return safeArea;
|
|
80
111
|
}
|
|
81
|
-
const overlapsCanvasHorizontally = towerRect.right > canvasRect.left && towerRect.left < canvasRect.right;
|
|
82
|
-
const hasVerticalBounds = Number.isFinite(canvasRect.top) &&
|
|
83
|
-
Number.isFinite(canvasRect.bottom) &&
|
|
84
|
-
Number.isFinite(towerRect.top) &&
|
|
85
|
-
Number.isFinite(towerRect.bottom);
|
|
86
|
-
const overlapsCanvasVertically = !hasVerticalBounds ||
|
|
87
|
-
(towerRect.bottom > canvasRect.top && towerRect.top < canvasRect.bottom);
|
|
88
112
|
const canvasWidth = Math.max(1, canvasRect.right - canvasRect.left || width);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
if (!overlapsCanvasHorizontally || !overlapsCanvasVertically || !isSideGutter) {
|
|
113
|
+
const towerRect = timingTowerFrameForCanvas(this.readouts, canvasRect, canvasWidth);
|
|
114
|
+
if (!towerRect) {
|
|
92
115
|
const safeArea = { left: 0, width };
|
|
93
116
|
this.safeAreaCache = { width, safeArea };
|
|
94
117
|
return safeArea;
|
|
@@ -149,12 +172,16 @@ export class CameraController {
|
|
|
149
172
|
}
|
|
150
173
|
|
|
151
174
|
isModeAvailable(mode, snapshot) {
|
|
152
|
-
|
|
175
|
+
if (!Object.hasOwn(CAMERA_PRESETS, mode)) return false;
|
|
176
|
+
if (mode === 'pit') return this.hasPitCamera(snapshot);
|
|
177
|
+
if (mode === 'driver') return this.driverCamera && Boolean(this.getFollowTarget(snapshot));
|
|
178
|
+
return true;
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
getFrame(snapshot, width, height, baseScale, safeArea = { left: 0, width }, selectedId = null) {
|
|
182
|
+
const activeSnapshot = snapshot ?? {};
|
|
156
183
|
const screenCenterX = safeArea.left + safeArea.width / 2;
|
|
157
|
-
const trackBounds = this.getTrackBounds(
|
|
184
|
+
const trackBounds = this.getTrackBounds(activeSnapshot.track);
|
|
158
185
|
const trackFitScale = this.getBoundsFitScale(trackBounds, height, safeArea);
|
|
159
186
|
|
|
160
187
|
if (this.camera.mode === 'overview') {
|
|
@@ -179,7 +206,7 @@ export class CameraController {
|
|
|
179
206
|
|
|
180
207
|
if (this.camera.mode === 'show-all') {
|
|
181
208
|
return this.applyFreeFrame(getShowAllCameraFrame({
|
|
182
|
-
cars:
|
|
209
|
+
cars: activeSnapshot.cars ?? [],
|
|
183
210
|
cameraZoom: this.camera.zoom,
|
|
184
211
|
height,
|
|
185
212
|
safeArea,
|
|
@@ -188,17 +215,29 @@ export class CameraController {
|
|
|
188
215
|
}));
|
|
189
216
|
}
|
|
190
217
|
|
|
191
|
-
if (this.camera.mode === 'pit' && this.hasPitCamera(
|
|
218
|
+
if (this.camera.mode === 'pit' && this.hasPitCamera(activeSnapshot)) {
|
|
192
219
|
return this.applyFreeFrame(
|
|
193
|
-
this.getPitFrame(
|
|
220
|
+
this.getPitFrame(activeSnapshot.track.pitLane, height, baseScale, safeArea, screenCenterX, trackFitScale),
|
|
194
221
|
);
|
|
195
222
|
}
|
|
196
223
|
|
|
224
|
+
if (this.camera.mode === 'driver' && this.driverCamera) {
|
|
225
|
+
const target = this.getFollowTarget(activeSnapshot, selectedId);
|
|
226
|
+
return this.applyFreeFrame({
|
|
227
|
+
target,
|
|
228
|
+
scale: clampCameraScale(baseScale * this.camera.zoom, baseScale, trackFitScale),
|
|
229
|
+
screenX: screenCenterX,
|
|
230
|
+
screenY: height * 0.68,
|
|
231
|
+
rotation: Number.isFinite(target?.heading) ? normalizeAngle(-Math.PI / 2 - target.heading) : 0,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
197
235
|
return this.applyFreeFrame({
|
|
198
|
-
target: this.getTarget(
|
|
236
|
+
target: this.getTarget(activeSnapshot, selectedId),
|
|
199
237
|
scale: clampCameraScale(baseScale * this.camera.zoom, baseScale, trackFitScale),
|
|
200
238
|
screenX: screenCenterX,
|
|
201
239
|
screenY: height / 2,
|
|
240
|
+
rotation: 0,
|
|
202
241
|
});
|
|
203
242
|
}
|
|
204
243
|
|
|
@@ -211,19 +250,27 @@ export class CameraController {
|
|
|
211
250
|
}
|
|
212
251
|
|
|
213
252
|
getTarget(snapshot, selectedId = null) {
|
|
253
|
+
const activeSnapshot = snapshot ?? {};
|
|
214
254
|
if (this.camera.free && this.camera.freeTarget) return this.camera.freeTarget;
|
|
215
|
-
if (this.camera.mode === 'overview') return this.getTrackTarget(
|
|
255
|
+
if (this.camera.mode === 'overview') return this.getTrackTarget(activeSnapshot.track);
|
|
216
256
|
|
|
217
|
-
if (this.camera.mode === 'pit' && this.hasPitCamera(
|
|
218
|
-
return this.getPitTarget(
|
|
257
|
+
if (this.camera.mode === 'pit' && this.hasPitCamera(activeSnapshot)) {
|
|
258
|
+
return this.getPitTarget(activeSnapshot.track.pitLane);
|
|
219
259
|
}
|
|
220
260
|
|
|
221
|
-
if (this.camera.mode === 'selected') {
|
|
222
|
-
|
|
223
|
-
if (selected) return selected;
|
|
261
|
+
if (this.camera.mode === 'selected' || this.camera.mode === 'driver') {
|
|
262
|
+
return this.getFollowTarget(activeSnapshot, selectedId);
|
|
224
263
|
}
|
|
225
264
|
|
|
226
|
-
|
|
265
|
+
return this.getFollowTarget(activeSnapshot);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
getFollowTarget(snapshot, selectedId = null) {
|
|
269
|
+
if (selectedId != null) {
|
|
270
|
+
const selected = snapshot?.cars?.find((car) => car.id === selectedId);
|
|
271
|
+
if (selected) return selected;
|
|
272
|
+
}
|
|
273
|
+
const leader = snapshot?.cars?.[0];
|
|
227
274
|
return leader ? { x: leader.x, y: leader.y } : { x: WORLD.width / 2, y: WORLD.height / 2 };
|
|
228
275
|
}
|
|
229
276
|
|
|
@@ -244,6 +291,7 @@ export class CameraController {
|
|
|
244
291
|
const frame = this.getFrame(snapshot, width, height, baseScale, safeArea, selectedId);
|
|
245
292
|
const scale = frame.scale;
|
|
246
293
|
const target = frame.target;
|
|
294
|
+
const targetRotation = Number.isFinite(frame.rotation) ? frame.rotation : 0;
|
|
247
295
|
const snapCamera = immediate || !this.camera.initialized;
|
|
248
296
|
this.camera.x = snapCamera
|
|
249
297
|
? target.x
|
|
@@ -254,12 +302,21 @@ export class CameraController {
|
|
|
254
302
|
const activeScale = snapCamera || this.camera.scale === null
|
|
255
303
|
? scale
|
|
256
304
|
: this.camera.scale + (scale - this.camera.scale) * CAMERA_SCALE_LERP;
|
|
305
|
+
const activeRotation = snapCamera
|
|
306
|
+
? targetRotation
|
|
307
|
+
: this.camera.rotation + normalizeAngle(targetRotation - this.camera.rotation) * CAMERA_ROTATION_LERP;
|
|
257
308
|
this.camera.initialized = true;
|
|
258
309
|
this.camera.scale = activeScale;
|
|
310
|
+
this.camera.rotation = activeRotation;
|
|
259
311
|
worldLayer.scale.set(activeScale);
|
|
312
|
+
worldLayer.rotation = activeRotation;
|
|
313
|
+
const cos = Math.cos(activeRotation);
|
|
314
|
+
const sin = Math.sin(activeRotation);
|
|
315
|
+
const transformedX = this.camera.x * cos - this.camera.y * sin;
|
|
316
|
+
const transformedY = this.camera.x * sin + this.camera.y * cos;
|
|
260
317
|
worldLayer.position.set(
|
|
261
|
-
frame.screenX -
|
|
262
|
-
frame.screenY -
|
|
318
|
+
frame.screenX - transformedX * activeScale,
|
|
319
|
+
frame.screenY - transformedY * activeScale,
|
|
263
320
|
);
|
|
264
321
|
}
|
|
265
322
|
}
|
|
@@ -74,7 +74,18 @@ export function getTrackCameraTarget(trackBounds) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function getShowAllCameraFrame({ cars, cameraZoom, height, safeArea, screenCenterX, trackFitScale }) {
|
|
77
|
-
const
|
|
77
|
+
const safeCars = Array.isArray(cars) ? cars : [];
|
|
78
|
+
if (safeCars.length === 0) {
|
|
79
|
+
const fallbackScale = clampCameraScale(trackFitScale ?? 1, trackFitScale ?? 1, trackFitScale);
|
|
80
|
+
return {
|
|
81
|
+
target: { x: WORLD.width / 2, y: WORLD.height / 2 },
|
|
82
|
+
scale: fallbackScale,
|
|
83
|
+
screenX: screenCenterX,
|
|
84
|
+
screenY: height / 2,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const bounds = safeCars.reduce((box, car) => ({
|
|
78
89
|
minX: Math.min(box.minX, car.x),
|
|
79
90
|
minY: Math.min(box.minY, car.y),
|
|
80
91
|
maxX: Math.max(box.maxX, car.x),
|
package/src/app/domBindings.js
CHANGED
|
@@ -1,15 +1,90 @@
|
|
|
1
|
+
function toArray(nodes) {
|
|
2
|
+
return [...(nodes ?? [])];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function resolveNodes(nodes, fallbackNode = null) {
|
|
6
|
+
const resolved = [];
|
|
7
|
+
if (typeof nodes?.forEach === 'function') {
|
|
8
|
+
nodes.forEach((node) => {
|
|
9
|
+
if (node && !resolved.includes(node)) resolved.push(node);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
if (resolved.length === 0 && fallbackNode) resolved.push(fallbackNode);
|
|
13
|
+
return resolved;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function queryRaceDataPanelBindings(root) {
|
|
17
|
+
const panelSelector = '[data-race-data-panel]';
|
|
18
|
+
const panels = toArray(root.querySelectorAll(panelSelector));
|
|
19
|
+
const fallbackPanel = panels.length ? null : root.querySelector(panelSelector);
|
|
20
|
+
const raceDataPanels = panels.length ? panels : (fallbackPanel ? [fallbackPanel] : []);
|
|
21
|
+
|
|
22
|
+
const findInPanel = (panel, selector) => panel?.querySelector?.(selector) ?? null;
|
|
23
|
+
const findFallback = (panel, selector) => findInPanel(panel, selector) ?? root.querySelector(selector);
|
|
24
|
+
const bindings = raceDataPanels.map((panel) => ({
|
|
25
|
+
panel,
|
|
26
|
+
kicker: findFallback(panel, '[data-race-data-kicker]'),
|
|
27
|
+
title: findFallback(panel, '[data-race-data-title]'),
|
|
28
|
+
number: findFallback(panel, '[data-race-data-number]'),
|
|
29
|
+
subtitle: findFallback(panel, '[data-race-data-subtitle]'),
|
|
30
|
+
open: findFallback(panel, '[data-race-data-open]'),
|
|
31
|
+
dismiss: findFallback(panel, '[data-race-data-dismiss]'),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const first = bindings[0] ?? {};
|
|
35
|
+
const fallbackOpen = first.open ?? root.querySelector('[data-race-data-open]');
|
|
36
|
+
const fallbackDismiss = first.dismiss ?? root.querySelector('[data-race-data-dismiss]');
|
|
37
|
+
return {
|
|
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
|
+
raceDataOpens: bindings.map((binding) => binding.open).filter(Boolean),
|
|
45
|
+
raceDataDismisses: bindings.map((binding) => binding.dismiss).filter(Boolean),
|
|
46
|
+
raceDataPanel: first.panel ?? null,
|
|
47
|
+
raceDataKicker: first.kicker ?? null,
|
|
48
|
+
raceDataTitle: first.title ?? null,
|
|
49
|
+
raceDataNumber: first.number ?? null,
|
|
50
|
+
raceDataSubtitle: first.subtitle ?? null,
|
|
51
|
+
raceDataOpen: fallbackOpen ?? null,
|
|
52
|
+
raceDataDismiss: fallbackDismiss ?? null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function queryCarOverviewFieldBindings(root) {
|
|
57
|
+
const fields = toArray(root.querySelectorAll('[data-overview-field]'));
|
|
58
|
+
return fields.map((field) => ({
|
|
59
|
+
field,
|
|
60
|
+
label: field.querySelector?.('[data-overview-field-label]') ?? null,
|
|
61
|
+
value: field.querySelector?.('[data-overview-field-value]') ?? null,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
1
65
|
export function querySimulatorDom(root) {
|
|
66
|
+
const raceDataBindings = queryRaceDataPanelBindings(root);
|
|
67
|
+
const carOverviewFieldBindings = queryCarOverviewFieldBindings(root);
|
|
2
68
|
const readouts = {
|
|
3
69
|
timingTower: root.querySelector('[data-timing-tower]'),
|
|
70
|
+
timingTowers: root.querySelectorAll('[data-timing-tower]'),
|
|
71
|
+
timingPanelToggle: root.querySelector('[data-timing-panel-toggle]'),
|
|
72
|
+
timingPanelToggles: root.querySelectorAll('[data-timing-panel-toggle]'),
|
|
4
73
|
mode: root.querySelector('[data-race-mode]'),
|
|
5
74
|
startLights: root.querySelector('[data-start-lights]'),
|
|
6
75
|
startLightsLabel: root.querySelector('[data-start-lights-label]'),
|
|
7
76
|
towerLap: root.querySelector('[data-tower-lap-readout]'),
|
|
77
|
+
towerLaps: root.querySelectorAll('[data-tower-lap-readout]'),
|
|
8
78
|
towerTotalLaps: root.querySelector('[data-tower-total-laps]'),
|
|
79
|
+
towerTotalLapsAll: root.querySelectorAll('[data-tower-total-laps]'),
|
|
9
80
|
towerRaceControlBanner: root.querySelector('[data-tower-race-control-banner]'),
|
|
81
|
+
towerRaceControlBanners: root.querySelectorAll('[data-tower-race-control-banner]'),
|
|
10
82
|
towerRaceControlKicker: root.querySelector('[data-tower-race-control-kicker]'),
|
|
83
|
+
towerRaceControlKickers: root.querySelectorAll('[data-tower-race-control-kicker]'),
|
|
11
84
|
towerRaceControlTitle: root.querySelector('[data-tower-race-control-title]'),
|
|
85
|
+
towerRaceControlTitles: root.querySelectorAll('[data-tower-race-control-title]'),
|
|
12
86
|
timingGapLabel: root.querySelector('[data-timing-gap-label]'),
|
|
87
|
+
timingGapLabels: root.querySelectorAll('[data-timing-gap-label]'),
|
|
13
88
|
lap: root.querySelector('[data-lap-readout]'),
|
|
14
89
|
drs: root.querySelector('[data-drs-readout]'),
|
|
15
90
|
contacts: root.querySelector('[data-contact-readout]'),
|
|
@@ -48,6 +123,7 @@ export function querySimulatorDom(root) {
|
|
|
48
123
|
carOverviewNumber: root.querySelector('[data-car-overview-number]'),
|
|
49
124
|
carOverviewCoreStat: root.querySelector('[data-car-overview-core-stat]'),
|
|
50
125
|
carOverviewFields: root.querySelectorAll('[data-overview-field]'),
|
|
126
|
+
carOverviewFieldBindings,
|
|
51
127
|
telemetryDrawerWorkbench: root.querySelector('[data-race-telemetry-drawer]'),
|
|
52
128
|
telemetryDrawer: root.querySelector('[data-telemetry-drawer]'),
|
|
53
129
|
telemetryDrawerToggle: root.querySelector('[data-telemetry-drawer-toggle]'),
|
|
@@ -55,13 +131,7 @@ export function querySimulatorDom(root) {
|
|
|
55
131
|
stewardMessageKicker: root.querySelector('[data-steward-message-kicker]'),
|
|
56
132
|
stewardMessageTitle: root.querySelector('[data-steward-message-title]'),
|
|
57
133
|
stewardMessageDetail: root.querySelector('[data-steward-message-detail]'),
|
|
58
|
-
|
|
59
|
-
raceDataKicker: root.querySelector('[data-race-data-kicker]'),
|
|
60
|
-
raceDataTitle: root.querySelector('[data-race-data-title]'),
|
|
61
|
-
raceDataNumber: root.querySelector('[data-race-data-number]'),
|
|
62
|
-
raceDataSubtitle: root.querySelector('[data-race-data-subtitle]'),
|
|
63
|
-
raceDataOpen: root.querySelector('[data-race-data-open]'),
|
|
64
|
-
raceDataDismiss: root.querySelector('[data-race-data-dismiss]'),
|
|
134
|
+
...raceDataBindings,
|
|
65
135
|
finishPanel: root.querySelector('[data-race-finish-panel]'),
|
|
66
136
|
finishWinner: root.querySelector('[data-race-finish-winner]'),
|
|
67
137
|
finishClassification: root.querySelector('[data-race-finish-classification]'),
|
|
@@ -71,15 +141,16 @@ export function querySimulatorDom(root) {
|
|
|
71
141
|
canvasHost: root.querySelector('[data-track-canvas]'),
|
|
72
142
|
safetyButtons: root.querySelectorAll('[data-safety-car]'),
|
|
73
143
|
restartButton: root.querySelector('[data-restart-race]'),
|
|
74
|
-
openButton:
|
|
144
|
+
openButton: raceDataBindings.raceDataOpen,
|
|
75
145
|
timingList: root.querySelector('[data-timing-list]'),
|
|
76
|
-
timingGapModeButtons: root.querySelectorAll('[data-timing-gap-
|
|
146
|
+
timingGapModeButtons: root.querySelectorAll('[data-timing-gap-toggle]'),
|
|
77
147
|
cameraButtons: root.querySelectorAll('[data-camera-mode]'),
|
|
78
148
|
overviewModeButtons: root.querySelectorAll('[data-overview-mode]'),
|
|
79
149
|
bannerMuteButtons: root.querySelectorAll('[data-race-data-banners-muted]'),
|
|
80
150
|
simulationSpeedButtons: root.querySelectorAll('[data-simulation-speed]'),
|
|
81
151
|
zoomInButton: root.querySelector('[data-zoom-in]'),
|
|
82
152
|
zoomOutButton: root.querySelector('[data-zoom-out]'),
|
|
153
|
+
timingLists: root.querySelectorAll('[data-timing-list]'),
|
|
83
154
|
readouts,
|
|
84
155
|
startLightNodes: [...(readouts.startLights?.querySelectorAll('.start-lights__gantry span') ?? [])],
|
|
85
156
|
};
|
|
@@ -91,6 +162,15 @@ export function setText(node, value) {
|
|
|
91
162
|
if (node.textContent !== nextValue) node.textContent = nextValue;
|
|
92
163
|
}
|
|
93
164
|
|
|
165
|
+
export function setStyleProperty(node, name, value) {
|
|
166
|
+
if (!node?.style?.setProperty) return;
|
|
167
|
+
const nextValue = String(value ?? '');
|
|
168
|
+
const currentValue = typeof node.style.getPropertyValue === 'function'
|
|
169
|
+
? node.style.getPropertyValue(name)
|
|
170
|
+
: null;
|
|
171
|
+
if (currentValue !== nextValue) node.style.setProperty(name, nextValue);
|
|
172
|
+
}
|
|
173
|
+
|
|
94
174
|
export function setTextAll(nodes, value) {
|
|
95
175
|
if (!nodes) return;
|
|
96
176
|
if (typeof nodes.forEach === 'function') {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function createRafScheduler(sync) {
|
|
2
|
+
let frame = null;
|
|
3
|
+
|
|
4
|
+
const run = () => {
|
|
5
|
+
frame = null;
|
|
6
|
+
sync();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
runNow: sync,
|
|
11
|
+
queue() {
|
|
12
|
+
if (frame !== null) return;
|
|
13
|
+
if (typeof requestAnimationFrame !== 'function') {
|
|
14
|
+
sync();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
frame = requestAnimationFrame(run);
|
|
18
|
+
},
|
|
19
|
+
cancel() {
|
|
20
|
+
if (frame === null) return;
|
|
21
|
+
if (typeof cancelAnimationFrame === 'function') {
|
|
22
|
+
cancelAnimationFrame(frame);
|
|
23
|
+
}
|
|
24
|
+
frame = null;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createRafScheduler } from './layoutScheduler.js';
|
|
2
|
+
|
|
3
|
+
export const PADDOCK_MIN_SUPPORTED_INLINE_SIZE = 320;
|
|
4
|
+
|
|
5
|
+
const MIN_INLINE_SIZE_BY_COMPONENT = {
|
|
6
|
+
'race-controls': 260,
|
|
7
|
+
'camera-controls': 160,
|
|
8
|
+
'safety-car-control': 44,
|
|
9
|
+
'timing-tower': 255,
|
|
10
|
+
'race-canvas': 180,
|
|
11
|
+
'race-telemetry-drawer': 280,
|
|
12
|
+
'telemetry-stack': 260,
|
|
13
|
+
'telemetry-core': 260,
|
|
14
|
+
'telemetry-sectors': 240,
|
|
15
|
+
'telemetry-lap-times': 240,
|
|
16
|
+
'telemetry-sector-times': 240,
|
|
17
|
+
'telemetry-sector-banner': 240,
|
|
18
|
+
'car-driver-overview': 260,
|
|
19
|
+
'race-data-panel': 260,
|
|
20
|
+
'steward-message': 240,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const MIN_BLOCK_SIZE_BY_COMPONENT = {
|
|
24
|
+
'race-controls': 88,
|
|
25
|
+
'camera-controls': 56,
|
|
26
|
+
'safety-car-control': 44,
|
|
27
|
+
'timing-tower': 220,
|
|
28
|
+
'race-canvas': 180,
|
|
29
|
+
'race-telemetry-drawer': 340,
|
|
30
|
+
'telemetry-stack': 180,
|
|
31
|
+
'telemetry-core': 150,
|
|
32
|
+
'telemetry-sectors': 96,
|
|
33
|
+
'telemetry-lap-times': 120,
|
|
34
|
+
'telemetry-sector-times': 140,
|
|
35
|
+
'telemetry-sector-banner': 88,
|
|
36
|
+
'car-driver-overview': 180,
|
|
37
|
+
'race-data-panel': 88,
|
|
38
|
+
'steward-message': 64,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function componentMinimumInlineSize(element) {
|
|
42
|
+
if (element.hasAttribute('data-f1-simulator-shell')) return PADDOCK_MIN_SUPPORTED_INLINE_SIZE;
|
|
43
|
+
const componentName = element.getAttribute('data-paddock-component');
|
|
44
|
+
if (componentName && MIN_INLINE_SIZE_BY_COMPONENT[componentName]) {
|
|
45
|
+
return MIN_INLINE_SIZE_BY_COMPONENT[componentName];
|
|
46
|
+
}
|
|
47
|
+
return PADDOCK_MIN_SUPPORTED_INLINE_SIZE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function componentMinimumBlockSize(element) {
|
|
51
|
+
const componentName = element.getAttribute('data-paddock-component');
|
|
52
|
+
if (componentName && MIN_BLOCK_SIZE_BY_COMPONENT[componentName]) {
|
|
53
|
+
return MIN_BLOCK_SIZE_BY_COMPONENT[componentName];
|
|
54
|
+
}
|
|
55
|
+
return element.hasAttribute('data-f1-simulator-shell') ? 360 : 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isRendered(element) {
|
|
59
|
+
if (
|
|
60
|
+
element.hidden ||
|
|
61
|
+
element.getAttribute('aria-hidden') === 'true' ||
|
|
62
|
+
element.classList?.contains('is-hidden')
|
|
63
|
+
) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const rect = element.getBoundingClientRect();
|
|
67
|
+
const style = getComputedStyle(element);
|
|
68
|
+
return rect.width > 0 &&
|
|
69
|
+
rect.height > 0 &&
|
|
70
|
+
style.display !== 'none' &&
|
|
71
|
+
style.visibility !== 'hidden';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function clearElementSupportState(element) {
|
|
75
|
+
if (element.dataset.paddockSizeUnsupported !== 'false') {
|
|
76
|
+
element.dataset.paddockSizeUnsupported = 'false';
|
|
77
|
+
}
|
|
78
|
+
delete element.dataset.paddockSizeUnsupportedReason;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function syncElementSupportState(element) {
|
|
82
|
+
if (!isRendered(element)) {
|
|
83
|
+
clearElementSupportState(element);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const rect = element.getBoundingClientRect();
|
|
87
|
+
const minInlineSize = componentMinimumInlineSize(element);
|
|
88
|
+
const minBlockSize = componentMinimumBlockSize(element);
|
|
89
|
+
const inlineUnsupported = rect.width < minInlineSize;
|
|
90
|
+
const blockUnsupported = minBlockSize > 0 && rect.height < minBlockSize;
|
|
91
|
+
const unsupported = inlineUnsupported || blockUnsupported;
|
|
92
|
+
const unsupportedValue = unsupported ? 'true' : 'false';
|
|
93
|
+
if (element.dataset.paddockSizeUnsupported !== unsupportedValue) {
|
|
94
|
+
element.dataset.paddockSizeUnsupported = unsupportedValue;
|
|
95
|
+
}
|
|
96
|
+
if (inlineUnsupported) {
|
|
97
|
+
if (element.dataset.paddockSizeUnsupportedReason !== 'inline') {
|
|
98
|
+
element.dataset.paddockSizeUnsupportedReason = 'inline';
|
|
99
|
+
}
|
|
100
|
+
} else if (blockUnsupported) {
|
|
101
|
+
if (element.dataset.paddockSizeUnsupportedReason !== 'block') {
|
|
102
|
+
element.dataset.paddockSizeUnsupportedReason = 'block';
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
delete element.dataset.paddockSizeUnsupportedReason;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function layoutSupportElements(root) {
|
|
110
|
+
const queryRoot = root?.children ? root : null;
|
|
111
|
+
if (!queryRoot) return [];
|
|
112
|
+
const elements = [];
|
|
113
|
+
if (root.matches?.('[data-f1-simulator-shell], [data-paddock-component]')) {
|
|
114
|
+
elements.push(root);
|
|
115
|
+
} else {
|
|
116
|
+
elements.push(
|
|
117
|
+
...[...queryRoot.children].filter((element) => (
|
|
118
|
+
element.matches?.('[data-f1-simulator-shell], [data-paddock-component]')
|
|
119
|
+
)),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return [...new Set(elements)];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function installLayoutSupport(root) {
|
|
126
|
+
const elements = layoutSupportElements(root);
|
|
127
|
+
const syncElements = () => elements.forEach(syncElementSupportState);
|
|
128
|
+
const scheduler = createRafScheduler(syncElements);
|
|
129
|
+
scheduler.runNow();
|
|
130
|
+
|
|
131
|
+
if (typeof ResizeObserver !== 'function') {
|
|
132
|
+
return () => {};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const observer = new ResizeObserver((entries) => {
|
|
136
|
+
if (typeof requestAnimationFrame !== 'function') {
|
|
137
|
+
entries.forEach((entry) => syncElementSupportState(entry.target));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
scheduler.queue();
|
|
141
|
+
});
|
|
142
|
+
elements.forEach((element) => observer.observe(element));
|
|
143
|
+
|
|
144
|
+
const mutationObserver = typeof MutationObserver === 'function'
|
|
145
|
+
? new MutationObserver(scheduler.queue)
|
|
146
|
+
: null;
|
|
147
|
+
elements.forEach((element) => {
|
|
148
|
+
mutationObserver?.observe(element, {
|
|
149
|
+
attributes: true,
|
|
150
|
+
attributeFilter: ['aria-hidden', 'class', 'hidden', 'style'],
|
|
151
|
+
childList: true,
|
|
152
|
+
subtree: true,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
157
|
+
scheduler.queue();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return () => {
|
|
161
|
+
observer.disconnect();
|
|
162
|
+
mutationObserver?.disconnect();
|
|
163
|
+
scheduler.cancel();
|
|
164
|
+
};
|
|
165
|
+
}
|