djmd 프레임 동기·오버레이 지면고정·선형-측점 정합·UI 단축키
- djmd(영상 내장 텔레메트리) 프레임 동기 위치/고도: MP4 stbl 선별 파싱(djmdTrack.ts), CSV 시각정렬 오차(평균 3.2m) 제거 — 세그먼트별 백그라운드 정밀화(frameSynced 플래그) - 자세 시간축 재정렬: djmd 위치 교차상관으로 CSV↔영상 오프셋 자동 추정(실측 -0.33s, 잔차 0.24m) 후 yaw/pitch/roll 재샘플(yaw 최단각) — 옆 지물 미끄러짐 해소 - 포즈 평활 자동 제한: 프레임 동기 데이터는 ±5프레임 상한(기존 ±60=±1s) — 회전 시 오버레이 전체가 드론에 딸려오는 지연 제거 - 선형-측점 정합: 측점 위치를 선형 정점으로 삽입(3D 이격≤0.012m) + 정점 z 를 측점 체이니지 보간으로 재정렬, 측점 라벨 화면 EMA 제거(선과 동일한 즉시 투영) — 선형이 측점 POI 를 픽셀 단위로 통과 - 드론높이 모드 높이 규칙 통일: 선형도 '드론고도-이격거리' 평면 적용(라벨과 동일) - 카메라 offZ 잔재 교정: 표고 체계 정비 이전 드래그 보정값(-18.3m)이 이중 보정이 되어 원거리 POI 가 접근할수록 밀리던 원인 — 경로 1-1 camera.json 0 으로 교정(.bak 백업) - 로드 UX: 폴더 로드 시 정지 상태로 대기(처음부터), 데이터 교체 시 UI 초기 상태 복원 - 라벨: 측점 팝업 표고(타원체고) 행 추가, 구조물-POI 이중 라벨 제거(어음천교), 시간동기 보정 슬라이더(timeOffsetSec) - 측점 표기 0+000 형식 전환(생성 6곳·파서 4곳 신구 호환) + 스테이션바 양끝 100m 내 시설물 없으면 시작/끝 스테이션 마크 - 단축키: F2 개발자 모드(dev_mode 1↔0, 세션 한정), A 선형 표시 토글 - 인프라: 54000 포트 재부팅 자동화(portproxy 직접 IP + 로그온 갱신, pm2 resurrect) - docs/history: 작업 이력 23건 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,11 +129,27 @@ const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
||||
// dev_mode (route.json) — 0 이면 '영상제어' 버튼과 제어 UI 전체를 숨김(뷰어 모드).
|
||||
// routeInfo.dev_mode 우선, 최상위 dev_mode 도 허용. 없으면 1(표시) — 기존 동작 유지.
|
||||
const storeRouteMeta = useGeoStore((s) => s.routeMeta);
|
||||
const devMode = (() => {
|
||||
const fileDevMode = (() => {
|
||||
const ri = storeRouteMeta?.routeInfo as Record<string, unknown> | undefined;
|
||||
const v = ri?.dev_mode ?? (storeRouteMeta as unknown as Record<string, unknown> | null)?.dev_mode;
|
||||
return v === 0 || v === '0' ? 0 : 1;
|
||||
})();
|
||||
// F2 → 개발자 모드(1↔0) 전환 — route.json 값을 세션 한정으로 덮어씀(파일 미변경).
|
||||
// 새 폴더 로드 시 초기화되어 다시 파일 값을 따른다. 텍스트 입력 중에는 무시.
|
||||
const [devModeOverride, setDevModeOverride] = useState<number | null>(null);
|
||||
useEffect(() => { setDevModeOverride(null); }, [storeRouteMeta]);
|
||||
const devMode = devModeOverride ?? fileDevMode;
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent): void => {
|
||||
if (e.code !== 'F2') return;
|
||||
const t = e.target as HTMLElement;
|
||||
if (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable) return;
|
||||
e.preventDefault();
|
||||
setDevModeOverride((prev) => ((prev ?? fileDevMode) === 0 ? 1 : 0));
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [fileDevMode]);
|
||||
// 분할 영상 재생목록 — 폴더 로드 시 이름 오름차순. 목록 클릭으로 세그먼트 직접 선택 재생.
|
||||
const videoFiles = useGeoStore((s) => s.videoFiles);
|
||||
const videoDurations = useGeoStore((s) => s.videoDurations);
|
||||
@@ -148,6 +164,18 @@ const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
||||
// 선형(중심선)·드론 궤적 영상 오버레이 토글 — StationOverlay 와 공유(설정 스토어).
|
||||
const showCenterline = useSettingsStore((s) => s.showCenterline);
|
||||
const setShowCenterline = useSettingsStore((s) => s.setShowCenterline);
|
||||
// A → 선형(중심선+측점 라벨) 표시/숨김 토글. 텍스트 입력·수정키 조합(Ctrl+A 등)은 무시.
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent): void => {
|
||||
if (e.code !== 'KeyA' || e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
const t = e.target as HTMLElement;
|
||||
if (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable) return;
|
||||
e.preventDefault();
|
||||
setShowCenterline(!showCenterline);
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [showCenterline, setShowCenterline]);
|
||||
const showDronePath = useSettingsStore((s) => s.showDronePath);
|
||||
const setShowDronePath = useSettingsStore((s) => s.setShowDronePath);
|
||||
const showRoutePanel = useSettingsStore((s) => s.showRoutePanel);
|
||||
@@ -195,13 +223,13 @@ const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
||||
if (usePlayerStore.getState().source?.kind !== 'local') return;
|
||||
if (i === geo.videoIndex) {
|
||||
playerRef.current?.currentTime(0);
|
||||
const p = playerRef.current?.play();
|
||||
if (p && typeof p.catch === 'function') p.catch(() => {});
|
||||
} else {
|
||||
const file = await geo.loadSegment(i);
|
||||
if (!file) return;
|
||||
loadLocalFile(file);
|
||||
loadLocalFile(file, true); // 목록 선택 — 로딩 완료 시 0초부터 자동 재생
|
||||
}
|
||||
const p = playerRef.current?.play();
|
||||
if (p && typeof p.catch === 'function') p.catch(() => {});
|
||||
};
|
||||
|
||||
// 분할 영상 연속재생 — 영상이 끝나면 같은 폴더의 다음 영상(이름 오름차순)을 자동 재생.
|
||||
@@ -219,10 +247,7 @@ const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
||||
if (next >= geo.videoFiles.length) return;
|
||||
const file = await geo.loadSegment(next);
|
||||
if (!file) return;
|
||||
loadLocalFile(file);
|
||||
// ended 직후는 paused 상태라 loadLocalFile 의 이어재생 로직이 안 걸림 → 직접 재생.
|
||||
const p = player.play();
|
||||
if (p && typeof p.catch === 'function') p.catch(() => {});
|
||||
loadLocalFile(file, true); // 연속재생 — 로딩 완료 시 0초부터 자동 재생
|
||||
})();
|
||||
};
|
||||
player.on('ended', onEnded);
|
||||
|
||||
Reference in New Issue
Block a user