import type { TrackOutline } from "@f1peak/viewer"; /** * World→scene transform: centre the circuit on the origin and scale its longest * span to ~100 scene units, so the camera framing is constant across circuits. * World Y (up) maps to scene −Z so the track reads upright from above. */ export function trackTransform(o: TrackOutline) { const { minX, minY, maxX, maxY } = o.bounds; const cx = (minX + maxX) / 2; const cy = (minY + maxY) / 2; const span = Math.max(maxX - minX, maxY - minY) || 1; const scale = 100 / span; const toScene = (x: number, y: number): [number, number] => [(x - cx) * scale, -(y - cy) * scale]; return { toScene, scale, span }; } /** * Synthesize a flat ribbon (triangle list, y=0) from the centerline by offsetting * each point ±halfWidth along its normal. HONEST: the feed has no width and no * elevation, so width is schematic and the ribbon is dead flat (captioned on screen). * `segFrac` carries each vertex's lap fraction (0..1) for future mini-sector colouring. */ export function buildRibbon(o: TrackOutline, halfWidth: number, toScene: (x: number, y: number) => [number, number]) { const n = o.x.length; const left: [number, number][] = []; const right: [number, number][] = []; for (let i = 0; i < n; i++) { const [ax, ay] = toScene(o.x[(i - 1 + n) % n], o.y[(i - 1 + n) % n]); const [bx, by] = toScene(o.x[(i + 1) % n], o.y[(i + 1) % n]); const [px, py] = toScene(o.x[i], o.y[i]); let dx = bx - ax; let dy = by - ay; const len = Math.hypot(dx, dy) || 1; dx /= len; dy /= len; const nx = -dy; const ny = dx; left.push([px + nx * halfWidth, py + ny * halfWidth]); right.push([px - nx * halfWidth, py - ny * halfWidth]); } const verts: number[] = []; const frac: number[] = []; for (let i = 0; i < n; i++) { const j = (i + 1) % n; const l0 = left[i]; const r0 = right[i]; const l1 = left[j]; const r1 = right[j]; verts.push(l0[0], 0, l0[1], r0[0], 0, r0[1], l1[0], 0, l1[1]); verts.push(r0[0], 0, r0[1], r1[0], 0, r1[1], l1[0], 0, l1[1]); const f = i / n; for (let k = 0; k < 6; k++) frac.push(f); } return { positions: new Float32Array(verts), segFrac: new Float32Array(frac) }; } /** centerline as scene points (slightly above the ribbon to avoid z-fighting) */ export function centerlinePoints(o: TrackOutline, toScene: (x: number, y: number) => [number, number]): [number, number, number][] { const n = o.x.length; const pts: [number, number, number][] = []; for (let i = 0; i <= n; i++) { const [x, z] = toScene(o.x[i % n], o.y[i % n]); pts.push([x, 0.06, z]); } return pts; } // ============================================================================ // Track 2 / Phase 1 — a track surface that READS as racing: real (schematic) // width, kerb bands, a run-off apron, and mini-sector vertex-colour via segFrac. // Still HONEST: the feed has no width/elevation, so width is schematic + the // ribbon is dead flat (captioned on screen). Geometry only — colour in Track3D. // ============================================================================ /** per-point scene position + unit normal (perpendicular to the local tangent) */ function frames(o: TrackOutline, toScene: (x: number, y: number) => [number, number]) { const n = o.x.length; const px: number[] = new Array(n); const py: number[] = new Array(n); const nx: number[] = new Array(n); const ny: number[] = new Array(n); for (let i = 0; i < n; i++) { const [ax, ay] = toScene(o.x[(i - 1 + n) % n], o.y[(i - 1 + n) % n]); const [bx, by] = toScene(o.x[(i + 1) % n], o.y[(i + 1) % n]); const [cx, cy] = toScene(o.x[i], o.y[i]); let dx = bx - ax; let dy = by - ay; const len = Math.hypot(dx, dy) || 1; dx /= len; dy /= len; px[i] = cx; py[i] = cy; nx[i] = -dy; ny[i] = dx; } return { n, px, py, nx, ny }; } /** triangle list for a flat band between two signed lateral offsets, at height y */ function band( f: ReturnType, off1: number, off2: number, y: number, ): Float32Array { const { n, px, py, nx, ny } = f; const verts: number[] = []; for (let i = 0; i < n; i++) { const j = (i + 1) % n; const a0x = px[i] + nx[i] * off1, a0y = py[i] + ny[i] * off1; const b0x = px[i] + nx[i] * off2, b0y = py[i] + ny[i] * off2; const a1x = px[j] + nx[j] * off1, a1y = py[j] + ny[j] * off1; const b1x = px[j] + nx[j] * off2, b1y = py[j] + ny[j] * off2; verts.push(a0x, y, a0y, b0x, y, b0y, a1x, y, a1y); verts.push(b0x, y, b0y, b1x, y, b1y, a1x, y, a1y); } return new Float32Array(verts); } export interface TrackGeo { runoff: Float32Array; surface: Float32Array; segFrac: Float32Array; // per surface vertex, lap fraction 0..1 kerb: Float32Array; kerbColors: Float32Array; // alternating red/white blocks edge: Float32Array; // thin white track-limit lines } /** * Layered flat track: run-off apron (widest, dark) → kerb bands (red/white) → * asphalt surface → thin white edge lines. Heights are staggered to avoid z-fighting. */ export function buildTrack( o: TrackOutline, toScene: (x: number, y: number) => [number, number], half = 1.6, ): TrackGeo { const f = frames(o, toScene); const K = half * 0.22; // kerb width const R = half * 1.5; // run-off apron width beyond the kerb const surface = band(f, -half, half, 0.02); const segFrac = new Float32Array((surface.length / 3) | 0); const per = segFrac.length / f.n; // 6 verts per segment for (let i = 0; i < f.n; i++) for (let k = 0; k < per; k++) segFrac[i * per + k] = i / f.n; // kerbs: a strip just outside each track limit, alternating red/white in blocks const kerbL = band(f, half, half + K, 0.028); const kerbR = band(f, -(half + K), -half, 0.028); const kerb = new Float32Array(kerbL.length + kerbR.length); kerb.set(kerbL, 0); kerb.set(kerbR, kerbL.length); const kerbColors = new Float32Array(kerb.length); // rgb per vertex const block = 4; // segments per colour block const paint = (base: Float32Array, off: number) => { for (let i = 0; i < f.n; i++) { const red = Math.floor(i / block) % 2 === 0; const r = red ? 0.86 : 0.92, g = red ? 0.16 : 0.92, b = red ? 0.13 : 0.92; for (let k = 0; k < 6; k++) { const v = (off + (i * 6 + k)) * 3; kerbColors[v] = r; kerbColors[v + 1] = g; kerbColors[v + 2] = b; } } }; paint(kerbL, 0); paint(kerbR, kerbL.length / 3); const runoffL = band(f, half + K, half + K + R, 0.0); const runoffR = band(f, -(half + K + R), -(half + K), 0.0); const runoff = new Float32Array(runoffL.length + runoffR.length); runoff.set(runoffL, 0); runoff.set(runoffR, runoffL.length); // thin white track-limit lines (just inside each kerb) const edgeL = band(f, half - half * 0.04, half, 0.022); const edgeR = band(f, -half, -(half - half * 0.04), 0.022); const edge = new Float32Array(edgeL.length + edgeR.length); edge.set(edgeL, 0); edge.set(edgeR, edgeL.length); return { runoff, surface, segFrac, kerb, kerbColors, edge }; } /** * A flat coloured strip ON the surface for one mini-sector arc [i0,i1) of the * outline — for the live "alive track" tint overlay (rebuilt on uiVersion). */ export function arcStrip( o: TrackOutline, toScene: (x: number, y: number) => [number, number], i0: number, i1: number, half: number, y = 0.05, ): Float32Array { const f = frames(o, toScene); const verts: number[] = []; for (let i = i0; i < i1; i++) { const a = (i + f.n) % f.n; const j = (i + 1 + f.n) % f.n; const w = half * 0.92; const a0x = f.px[a] + f.nx[a] * -w, a0y = f.py[a] + f.ny[a] * -w; const b0x = f.px[a] + f.nx[a] * w, b0y = f.py[a] + f.ny[a] * w; const a1x = f.px[j] + f.nx[j] * -w, a1y = f.py[j] + f.ny[j] * -w; const b1x = f.px[j] + f.nx[j] * w, b1y = f.py[j] + f.ny[j] * w; verts.push(a0x, y, a0y, b0x, y, b0y, a1x, y, a1y); verts.push(b0x, y, b0y, b1x, y, b1y, a1x, y, a1y); } return new Float32Array(verts); }