fix: 중심선 스무딩 적용 및 디버그 패널 제거

- renderCacheRef useEffect에서 CL 계산 시 smoothFrame 적용
  (텍스트와 동일한 smooth 슬라이더 값 공유)
- currentFrameIdxRef 추가로 smoothFrame 인덱스 전달
- 디버그 패널(frame/frac/mapSize) 캔버스에서 제거

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-01 15:22:56 +09:00
parent 2aae3d1c0d
commit 65504cae2b

View File

@@ -114,6 +114,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
// 현재 상태 ref
const currentDroneFrameRef = useRef<DroneFrameBasic | null>(null);
const currentFrameNumRef = useRef<number>(0); // RAF에서 Map 조회용
const currentFrameIdxRef = useRef<number>(0); // smoothFrame용 배열 인덱스
const currentTimeSecRef = useRef<number>(0); // 마지막으로 알려진 재생 시간
const timeUpdateWallRef = useRef<number>(performance.now()); // currentTime 갱신된 시각
const paramsRef = useRef<CameraParams>(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;