feat: RoutePanel 미니맵 추가 및 StationOverlay altitude 필드명 수정

- RoutePanel: 세로 미니맵 패널 — 측점/POI 위치 표시, 오렌지 마커 드래그로 seek
- VideoPlayer: RoutePanel 통합 (showStations 토글 연동)
- StationOverlay: smoothFrame에서 alt → altitude 타입 오류 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-01 15:59:03 +09:00
parent 65504cae2b
commit 8ddc148ef9
3 changed files with 357 additions and 9 deletions

View File

@@ -41,10 +41,19 @@ interface Props {
visible: boolean;
}
// category → 이모지
const CATEGORY_EMOJI: Record<string, string> = {
'터널': '🚇',
'교량': '🌉',
'역사': '🚉',
'지장물': '🏢',
'측점': '📍',
};
// 텍스트 사전 계산 캐시 (Map<frameNum, LabelCache>)
interface LabelCache {
stationLabels: { sx: number; sy: number; title: string }[];
poiMarkers: { x: number; y: number; title: string }[];
poiMarkers: { x: number; y: number; title: string; category: string }[];
}
// 중심선 + 나침반 렌더 캐시 (per-frame, renderCacheRef)
@@ -205,17 +214,17 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
const lo = Math.max(0, i - halfWin);
const hi = Math.min(frames.length - 1, i + halfWin);
const n = hi - lo + 1;
let lat = 0, lon = 0, alt = 0, pitch = 0, roll = 0, sinYaw = 0, cosYaw = 0;
let lat = 0, lon = 0, altitude = 0, pitch = 0, roll = 0, sinYaw = 0, cosYaw = 0;
for (let k = lo; k <= hi; k++) {
const f = frames[k];
lat += f.lat; lon += f.lon; alt += f.alt;
lat += f.lat; lon += f.lon; altitude += f.altitude;
pitch += f.pitch; roll += f.roll;
const yr = f.yaw * Math.PI / 180;
sinYaw += Math.sin(yr); cosYaw += Math.cos(yr);
}
return {
...frames[i],
lat: lat / n, lon: lon / n, alt: alt / n,
lat: lat / n, lon: lon / n, altitude: altitude / n,
pitch: pitch / n, roll: roll / n,
yaw: Math.atan2(sinYaw / n, cosYaw / n) * 180 / Math.PI,
};
@@ -262,7 +271,7 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
if (cc.Zc < CLIP_Z) continue;
const { pxRaw, pyRaw } = pixelFromCamera(cc, currentParams);
if (pxRaw < -0.02 || pxRaw > 1.02 || pyRaw < -0.02 || pyRaw > 1.02) continue;
poiMarkers.push({ x: pxRaw, y: pyRaw, title: poi.title });
poiMarkers.push({ x: pxRaw, y: pyRaw, title: poi.title, category: poi.category });
}
newMap.set(drone.frame, { stationLabels, poiMarkers });
@@ -484,15 +493,16 @@ export default function StationOverlay({ currentFrame, currentTime, fps, visible
ctx.moveTo(px-r, py); ctx.lineTo(px+r, py);
ctx.moveTo(px, py-r); ctx.lineTo(px, py+r);
ctx.stroke();
// 텍스트 테두리
// 이모지 + 텍스트
const emoji = CATEGORY_EMOJI[poiA.category] ?? '📌';
const label = `${emoji} ${poiA.title}`;
const lx = Math.max(2, px + 14);
ctx.strokeStyle = 'rgba(0,0,0,0.85)'; ctx.lineWidth = 4;
ctx.lineJoin = 'round';
ctx.textBaseline = 'middle';
ctx.strokeText(poiA.title, lx, py);
// 텍스트 본문
ctx.strokeText(label, lx, py);
ctx.fillStyle = '#64c8ff';
ctx.fillText(poiA.title, lx, py);
ctx.fillText(label, lx, py);
ctx.textBaseline = 'alphabetic';
});
}