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:
2026-07-14 17:59:43 +09:00
co-authored by Claude Fable 5
parent 7d51ba9945
commit c64c57b58e
36 changed files with 1248 additions and 56 deletions
+30 -8
View File
@@ -123,7 +123,7 @@ const clamp = (v: number, lo: number, hi: number): number =>
Math.min(hi, Math.max(lo, v));
function stationKm(title: string): number {
const m = title.match(/(\d+)[Kk](\d+)/);
const m = title.match(/(\d+)\s*[Kk+]\s*(\d+)/);
return m ? parseInt(m[1], 10) * 1000 + parseInt(m[2], 10) : -1;
}
@@ -144,26 +144,26 @@ function nearestStationKm(f: DroneFrameBasic, stations: GeoPoint[]): number {
/** station 값(숫자=미터, "158k400" 문자열) → 미터. 실패 시 -1. */
function mileageToMeters(v: number | string): number {
if (typeof v === 'number') return v;
const m = String(v).match(/(\d+)\s*[kK]\s*(\d+)/);
const m = String(v).match(/(\d+)\s*[kK+]\s*(\d+)/);
if (m) return parseInt(m[1], 10) * 1000 + parseInt(m[2], 10);
const n = parseInt(String(v).replace(/[^0-9]/g, ''), 10);
return Number.isFinite(n) ? n : -1;
}
/** 미터값 → "158k710" (10m 단위 반올림). 커서 배지용 십단위 표시. */
/** 미터값 → "158+710" (10m 단위 반올림). 커서 배지용 십단위 표시. */
function formatMileage10(m: number): string {
const r = Math.round(m / 10) * 10;
const km = Math.floor(r / 1000);
const mm = r % 1000;
return `${km}k${String(mm).padStart(3, '0')}`;
return `${km}+${String(mm).padStart(3, '0')}`;
}
/** 미터값 → "158k163" (1m 단위) — 커서 배지용. 노선밖 표기(1m)와 갱신 밀도를 맞춘다. */
/** 미터값 → "158+163" (1m 단위) — 커서 배지용. 노선밖 표기(1m)와 갱신 밀도를 맞춘다. */
function formatMileage1(m: number): string {
const r = Math.round(m);
const km = Math.floor(r / 1000);
const mm = r % 1000;
return `${km}k${String(mm).padStart(3, '0')}`;
return `${km}+${String(mm).padStart(3, '0')}`;
}
/**
@@ -659,6 +659,28 @@ export function StationBar({
return marks;
};
// 바 양끝 시작/끝 스테이션 — 각 끝 100m(체이니지) 이내에 다른 마크가 없으면
// 영상 시작·끝 지점의 측점값 마크를 추가(양끝이 비어 시작/끝 위치가 모호한 것 방지).
// 노선 밖(접근/이탈) 프레임은 체이니지가 클램프되므로 첫/마지막 '노선 위' 프레임 기준.
const appendEndpointStations = (marks: StructMark[]): StructMark[] => {
const va = viewedRef.current;
if (!va.length) return marks;
const onRoute = va.filter(v => v.offM <= OFF_ROUTE_M);
if (!onRoute.length) return marks;
const ends = [
{ v: onRoute[0], title: '시작' },
{ v: onRoute[onRoute.length - 1], title: '끝' },
];
for (const e of ends) {
const ch = e.v.chain;
if (marks.some(m => Math.abs(m.km - ch) <= 100)) continue;
const px = pxAtTime(e.v.time);
if (px === null) continue;
marks.push({ px, title: e.title, category: '측점', km: Math.round(ch), kmExists: true });
}
return marks;
};
// 구조물 후보 POI(교량/터널/역사) base 이름 → 실좌표.
const cats = new Set(['교량', '터널', '역사']);
const poiByName = new Map<string, { lat: number; lon: number; category: string }>();
@@ -741,7 +763,7 @@ export function StationBar({
out.push({ px: p.px, title: s.name, category: cat, km: stationKmVal ?? p.km, kmExists, props: s.props });
}
}
return placeUnreachedTerminal(appendStartMark(out));
return appendEndpointStations(placeUnreachedTerminal(appendStartMark(out)));
}
// 폴백: POI category 기반 (실좌표 GPS 근접, 모든 통과).
@@ -760,7 +782,7 @@ export function StationBar({
out.push({ px: p.px, title: base, category: info.category, km: stationKmVal ?? p.km, kmExists });
}
}
return placeUnreachedTerminal(appendStartMark(out));
return appendEndpointStations(placeUnreachedTerminal(appendStartMark(out)));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ready, duration, storeFrames, pois, stations, pxAtTime, routeMeta, routeDirCh, storeStructures, pxAtMileage, endGapPx, stationLine, videoFps]);