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:
@@ -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';
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user