mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
Spike: replay location fan-out is bounded with a semaphore at 4 concurrent GetLocation calls, covered by TestAssembleReplayFramesBoundsLocationFanOut. Adds /api/v1/replay/frames with 5s interval floor, 3000-frame cap, nearest-sample snapping, and empty-driver omission. Wires RaceStoryCanvas to lazy-load replay frames plus cached track outlines when the map panel opens, sharing the existing scrubber/playback state across chart and map. Tests: HOME=/private/tmp/box-box-home GOCACHE=/private/tmp/box-box-go-cache GOMODCACHE=/Users/aman/go/pkg/mod go test ./internal/web; go build ./... (same env, passed with read-only module stat-cache warning); npm run test; npx tsc --noEmit. Full go test ./... is blocked in this sandbox by existing network/listener-dependent internal/api and internal/news tests.
76 lines
2.2 KiB
TypeScript
76 lines
2.2 KiB
TypeScript
import type { ReplayCarPosition, ReplayFrame, TrackBounds } from '../types'
|
|
import { canvasToSvg, normalizeRawPoint } from './trackmap'
|
|
|
|
export interface ReplayFramePair {
|
|
previous: ReplayFrame | null
|
|
next: ReplayFrame | null
|
|
}
|
|
|
|
export function lookupReplayFramePair(
|
|
frames: ReadonlyArray<ReplayFrame>,
|
|
tMs: number,
|
|
): ReplayFramePair {
|
|
if (frames.length === 0) return { previous: null, next: null }
|
|
if (tMs <= frames[0].t) return { previous: frames[0], next: frames[0] }
|
|
const last = frames[frames.length - 1]
|
|
if (tMs >= last.t) return { previous: last, next: last }
|
|
|
|
let lo = 0
|
|
let hi = frames.length - 1
|
|
while (lo <= hi) {
|
|
const mid = Math.floor((lo + hi) / 2)
|
|
const frame = frames[mid]
|
|
if (frame.t === tMs) return { previous: frame, next: frame }
|
|
if (frame.t < tMs) {
|
|
lo = mid + 1
|
|
} else {
|
|
hi = mid - 1
|
|
}
|
|
}
|
|
|
|
return { previous: frames[Math.max(0, hi)], next: frames[Math.min(frames.length - 1, lo)] }
|
|
}
|
|
|
|
export function interpolateReplayCars(
|
|
frames: ReadonlyArray<ReplayFrame>,
|
|
tMs: number,
|
|
): Record<string, ReplayCarPosition> {
|
|
const { previous, next } = lookupReplayFramePair(frames, tMs)
|
|
if (!previous && !next) return {}
|
|
if (!previous) return next?.cars ?? {}
|
|
if (!next || previous.t === next.t) return previous.cars
|
|
|
|
const span = next.t - previous.t
|
|
const fraction = span <= 0 ? 0 : clamp01((tMs - previous.t) / span)
|
|
const cars: Record<string, ReplayCarPosition> = {}
|
|
const numbers = new Set([...Object.keys(previous.cars), ...Object.keys(next.cars)])
|
|
|
|
for (const number of numbers) {
|
|
const before = previous.cars[number]
|
|
const after = next.cars[number]
|
|
if (before && after) {
|
|
cars[number] = {
|
|
x: before.x + (after.x - before.x) * fraction,
|
|
y: before.y + (after.y - before.y) * fraction,
|
|
}
|
|
} else if (before) {
|
|
cars[number] = before
|
|
} else if (after) {
|
|
cars[number] = after
|
|
}
|
|
}
|
|
return cars
|
|
}
|
|
|
|
export function replayCarToSvg(
|
|
car: ReplayCarPosition,
|
|
bounds: TrackBounds,
|
|
): ReplayCarPosition {
|
|
return canvasToSvg(normalizeRawPoint({ x: car.x, y: car.y }, bounds))
|
|
}
|
|
|
|
function clamp01(value: number): number {
|
|
if (!Number.isFinite(value)) return 0
|
|
return Math.min(1, Math.max(0, value))
|
|
}
|