From a993387ae2931394bdb75aaaf25ed8c57be93df3 Mon Sep 17 00:00:00 2001 From: Korupt-Virus Date: Tue, 11 Aug 2026 15:20:40 -0700 Subject: [PATCH] fix: restore iRacing analyse car pointer --- CHANGELOG.md | 1 + .../src/components/analyse/track-map/path.ts | 40 +++++++++++++++++-- client/test/static-track-drawing.test.ts | 29 ++++++++++++++ server/routes/laps/resource-routes.ts | 1 + test/routes/lap-analysis-route.test.ts | 3 +- 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84823da4..724a082e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Ignore one-frame iRacing lap-counter resets that created invalid duplicate lap numbers in session recaps - Show iRacing steering direction and signed values correctly in live views, Analyse, Compare, and saved recordings - Draw iRacing left-turning oval laps in the correct direction on Analyse track maps +- Restore the moving car pointer on iRacing Analyse track maps - Honor Analyse and Compare URL state so saved chats open with their AI panel visible and comparison cursor links are preserved - Restore experiment version loading, editing, deletion, and recovery after the version API rename - Keep Analyse insight navigation aligned on desktop and move the timeline tracking bar when stepping through events diff --git a/client/src/components/analyse/track-map/path.ts b/client/src/components/analyse/track-map/path.ts index c658d0ef..d831e481 100644 --- a/client/src/components/analyse/track-map/path.ts +++ b/client/src/components/analyse/track-map/path.ts @@ -3,10 +3,44 @@ import type { Point, SemanticAnalysisFrame } from "./types"; const number = (frame: SemanticAnalysisFrame, id: keyof SemanticAnalysisFrame["values"]) => { const value = frame.values[id]; return typeof value === "number" && Number.isFinite(value) ? value : null; -} +}; + +const worldPosition = (frame: SemanticAnalysisFrame): Point => ({ + x: number(frame, "motion.position-x") ?? 0, + z: number(frame, "motion.position-z") ?? 0, +}); + +export function resolveTrackPositions(telemetry: SemanticAnalysisFrame[], outline: Point[] | null): Point[] { + const worldPositions = telemetry.map(worldPosition); + if (worldPositions.some((point) => point.x !== 0 || point.z !== 0) || !outline || outline.length < 2) return worldPositions; + + const fractions = telemetry.map((frame) => number(frame, "timing.lap-fraction")); + if (fractions.some((fraction) => fraction === null)) return worldPositions; -export function resolveTrackPositions(telemetry: SemanticAnalysisFrame[], _outline: Point[] | null): Point[] { - return telemetry.map((frame) => ({ x: number(frame, "motion.position-x") ?? 0, z: number(frame, "motion.position-z") ?? 0 })); + const cumulative = [0]; + for (let index = 1; index < outline.length; index++) { + cumulative.push(cumulative[index - 1] + Math.hypot(outline[index].x - outline[index - 1].x, outline[index].z - outline[index - 1].z)); + } + const total = cumulative.at(-1) ?? 0; + if (total <= 0) return worldPositions; + + return fractions.map((fraction) => { + const target = Math.max(0, Math.min(1, fraction!)) * total; + let low = 1; + let high = cumulative.length - 1; + while (low < high) { + const middle = (low + high) >> 1; + if (cumulative[middle] < target) low = middle + 1; + else high = middle; + } + const start = Math.max(0, low - 1); + const segmentLength = cumulative[low] - cumulative[start]; + const amount = segmentLength > 0 ? (target - cumulative[start]) / segmentLength : 0; + return { + x: outline[start].x + (outline[low].x - outline[start].x) * amount, + z: outline[start].z + (outline[low].z - outline[start].z) * amount, + }; + }); } export function pathForwardOffsets(points: readonly Point[]): ([number, number] | null)[] { diff --git a/client/test/static-track-drawing.test.ts b/client/test/static-track-drawing.test.ts index d4d9b5a9..1e1b150e 100644 --- a/client/test/static-track-drawing.test.ts +++ b/client/test/static-track-drawing.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "bun:test"; import { drawStaticTrack } from "../src/components/analyse/track-map/static-drawing"; +import { resolveTrackPositions } from "../src/components/analyse/track-map/path"; test("returns no transform when replay has no drawable track points", () => { const previousWindow = globalThis.window; @@ -29,6 +30,34 @@ test("returns no transform when replay has no drawable track points", () => { Object.defineProperty(globalThis, "window", { configurable: true, value: previousWindow }); } }); +test("projects telemetry without world coordinates onto the track outline", () => { + const frame = (fraction: number) => ({ + values: { "motion.position-x": null, "motion.position-z": null, "timing.lap-fraction": fraction }, + states: {}, + freshness: {}, + }); + const outline = [{ x: 0, z: 0 }, { x: 100, z: 0 }, { x: 100, z: 100 }]; + + expect(resolveTrackPositions([frame(0), frame(0.25), frame(0.75), frame(1)], outline)).toEqual([ + { x: 0, z: 0 }, + { x: 50, z: 0 }, + { x: 100, z: 50 }, + { x: 100, z: 100 }, + ]); +}); + +test("prefers recorded world coordinates over lap-fraction projection", () => { + const frame = (x: number, z: number, fraction: number) => ({ + values: { "motion.position-x": x, "motion.position-z": z, "timing.lap-fraction": fraction }, + states: {}, + freshness: {}, + }); + + expect(resolveTrackPositions( + [frame(20, 30, 0), frame(40, 50, 1)], + [{ x: 0, z: 0 }, { x: 100, z: 0 }], + )).toEqual([{ x: 20, z: 30 }, { x: 40, z: 50 }]); +}); test("draws throttle input traces in the throttle channel color", () => { const previousWindow = globalThis.window; diff --git a/server/routes/laps/resource-routes.ts b/server/routes/laps/resource-routes.ts index 143413c6..219d6316 100644 --- a/server/routes/laps/resource-routes.ts +++ b/server/routes/laps/resource-routes.ts @@ -40,6 +40,7 @@ export function semanticReplayIds(): readonly string[] { "timing.current-lap", "timing.current-race-time", "timing.distance-traveled", + "timing.lap-fraction", "aero.drs-active", "weather.air-temp", "fuel.ers-store-energy", diff --git a/test/routes/lap-analysis-route.test.ts b/test/routes/lap-analysis-route.test.ts index 99c29099..8440a496 100644 --- a/test/routes/lap-analysis-route.test.ts +++ b/test/routes/lap-analysis-route.test.ts @@ -7,8 +7,9 @@ import { semanticReplayIds } from "../../server/routes/laps/resource-routes"; initGameAdapters(); -test("semantic replay requests lap-relative timing", () => { +test("semantic replay requests lap-relative timing and position", () => { expect(semanticReplayIds()).toContain("timing.current-lap"); + expect(semanticReplayIds()).toContain("timing.lap-fraction"); }); test("semantic replay requests every Analyse Data panel dependency", () => { const ids = semanticReplayIds();