import * as THREE from "three"; export const ROAD_WIDTH = 16; export const trackCurve = new THREE.CatmullRomCurve3( [ // מסלול אובלי קצר: שתי ישורות ושתי פניות רחבות בלבד. new THREE.Vector3(-58, 0, -48), // קו הזינוק באמצע הישורת new THREE.Vector3(-54, 0, -88), new THREE.Vector3(-32, 0, -112), new THREE.Vector3(0, 0, -116), new THREE.Vector3(32, 0, -112), new THREE.Vector3(54, 0, -88), new THREE.Vector3(58, 0, -48), new THREE.Vector3(58, 0, -12), new THREE.Vector3(32, 0, 12), new THREE.Vector3(0, 0, 18), new THREE.Vector3(-32, 0, 12), new THREE.Vector3(-58, 0, -12), ], true, "catmullrom", 0.5, ); const SAMPLES = 700; export const trackSamples: { p: THREE.Vector3; t: number }[] = Array.from( { length: SAMPLES }, (_, i) => { const t = i / SAMPLES; return { p: trackCurve.getPointAt(t), t }; }, ); /** Nearest normalized position (0..1) along the track and distance from centerline. */ export function nearestOnTrack(pos: THREE.Vector3) { let best = 0; let bestDist = Infinity; for (const s of trackSamples) { const dx = s.p.x - pos.x; const dz = s.p.z - pos.z; const d = dx * dx + dz * dz; if (d < bestDist) { bestDist = d; best = s.t; } } return { t: best, distance: Math.sqrt(bestDist) }; } /** Flat ribbon geometry following the curve. */ export function buildRoadGeometry(width = ROAD_WIDTH, segments = 700) { const positions: number[] = []; const uvs: number[] = []; const indices: number[] = []; const up = new THREE.Vector3(0, 1, 0); const side = new THREE.Vector3(); for (let i = 0; i <= segments; i++) { const t = i / segments; const p = trackCurve.getPointAt(t % 1); const tan = trackCurve.getTangentAt(t % 1); side.crossVectors(tan, up).normalize().multiplyScalar(width / 2); positions.push(p.x - side.x, 0.02, p.z - side.z); positions.push(p.x + side.x, 0.02, p.z + side.z); uvs.push(0, t * 60, 1, t * 60); if (i < segments) { const a = i * 2; indices.push(a, a + 1, a + 2, a + 1, a + 3, a + 2); } } const geo = new THREE.BufferGeometry(); geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geo.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2)); geo.setIndex(indices); geo.computeVertexNormals(); return geo; } /** Thin colored strip along one edge of the road (kerb). */ export function buildEdgeGeometry(offset: number, thickness = 0.9, segments = 700) { const positions: number[] = []; const indices: number[] = []; const up = new THREE.Vector3(0, 1, 0); const side = new THREE.Vector3(); for (let i = 0; i <= segments; i++) { const t = i / segments; const p = trackCurve.getPointAt(t % 1); const tan = trackCurve.getTangentAt(t % 1); side.crossVectors(tan, up).normalize(); const inner = offset - thickness / 2; const outer = offset + thickness / 2; positions.push(p.x + side.x * inner, 0.05, p.z + side.z * inner); positions.push(p.x + side.x * outer, 0.05, p.z + side.z * outer); if (i < segments) { const a = i * 2; indices.push(a, a + 1, a + 2, a + 1, a + 3, a + 2); } } const geo = new THREE.BufferGeometry(); geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geo.setIndex(indices); geo.computeVertexNormals(); return geo; } export const startPoint = trackCurve.getPointAt(0); export const startTangent = trackCurve.getTangentAt(0); export const startHeading = Math.atan2(startTangent.x, startTangent.z);