diff --git a/client/src/components/overlay/StationOverlay.tsx b/client/src/components/overlay/StationOverlay.tsx index e462578..e89faed 100644 --- a/client/src/components/overlay/StationOverlay.tsx +++ b/client/src/components/overlay/StationOverlay.tsx @@ -114,6 +114,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible // 현재 상태 ref const currentDroneFrameRef = useRef(null); const currentFrameNumRef = useRef(0); // RAF에서 Map 조회용 + const currentFrameIdxRef = useRef(0); // smoothFrame용 배열 인덱스 const currentTimeSecRef = useRef(0); // 마지막으로 알려진 재생 시간 const timeUpdateWallRef = useRef(performance.now()); // currentTime 갱신된 시각 const paramsRef = useRef(DEFAULT_CAMERA_PARAMS); @@ -298,13 +299,14 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible if (!visible || !droneFramesLoaded) return; const frames = allDroneFramesRef.current; if (!frames.length) return; - let best = frames[0], bestD = Math.abs((best.frame ?? 0) / VIDEO_FPS - currentTime); - for (const f of frames) { - const d = Math.abs(f.frame / VIDEO_FPS - currentTime); - if (d < bestD) { bestD = d; best = f; } + let best = frames[0], bestIdx = 0, bestD = Math.abs((best.frame ?? 0) / VIDEO_FPS - currentTime); + for (let i = 0; i < frames.length; i++) { + const d = Math.abs(frames[i].frame / VIDEO_FPS - currentTime); + if (d < bestD) { bestD = d; best = frames[i]; bestIdx = i; } if (bestD < 1 / VIDEO_FPS / 2) break; } currentFrameNumRef.current = best.frame; + currentFrameIdxRef.current = bestIdx; currentTimeSecRef.current = currentTime; timeUpdateWallRef.current = performance.now(); if (currentDroneFrameRef.current?.frame !== best.frame) { @@ -315,10 +317,14 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible // 중심선 + 나침반 캐시 빌드 (per-frame, 텍스트 계산 없음) useEffect(() => { - const drone = currentDroneFrameRef.current; - if (!drone || !visible) { renderCacheRef.current = null; return; } + if (!currentDroneFrameRef.current || !visible) { renderCacheRef.current = null; return; } const t0 = performance.now(); + // 중심선도 smoothFrame 적용 (텍스트와 동일한 스무딩) + const frames = allDroneFramesRef.current; + const drone = frames.length + ? smoothFrame(frames, currentFrameIdxRef.current, smoothHalfRef.current) + : currentDroneFrameRef.current; const params = paramsRef.current; const worldOrigin = worldOriginRef.current; const allCL = allCenterlinePointsRef.current; @@ -426,28 +432,12 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible const elapsed = (performance.now() - timeUpdateWallRef.current) / 1000; const estTime = currentTimeSecRef.current + elapsed; const frac = Math.min(0.999, (estTime * VIDEO_FPS) - frameNum); // 0~0.999 - const mapSize = labelMapRef.current.size; const labelsA = labelMapRef.current.get(frameNum); const labelsB = labelMapRef.current.get(frameNum + 1); // 두 프레임 사이 픽셀 좌표 보간 const interpY = (a: number, b: number | undefined) => b !== undefined ? a + (b - a) * frac : a; - // 디버그 표시 (좌하단) - ctx.font = '11px monospace'; - ctx.textAlign = 'left'; - ctx.textBaseline = 'top'; - ctx.fillStyle = 'rgba(0,0,0,0.6)'; - ctx.fillRect(6, H - 54, 300, 48); - ctx.fillStyle = '#0f0'; - ctx.fillText(`frame: ${frameNum} frac: ${frac.toFixed(2)} mapSize: ${mapSize}`, 10, H - 50); - const firstSt = labelsA?.stationLabels[0]; - const firstStB = labelsB?.stationLabels[0]; - const dispY = firstSt ? interpY(firstSt.sy, firstStB?.sy) * H : 0; - ctx.fillText(`labels: ${labelsA?.stationLabels.length ?? '-'} firstY: ${firstSt ? dispY.toFixed(1) : '-'}px`, 10, H - 36); - ctx.fillText(`smooth: ±${smoothHalfRef.current}fr interp: ${frac.toFixed(2)}`, 10, H - 22); - ctx.textBaseline = 'alphabetic'; - if (labelsA) { const α = emaAlphaRef.current;