스테이션바 시선 연동(station_lookahead)·팬 지연 보정·1km 측점 마크
- station_lookahead(route.json): 배지 전방 오프셋 — 미설정/0=드론 연직(기존), N=진행방향 +N(m), "auto"=짐벌 피치·AGL 기반 자동. 연속 모델(유효 주시거리 = N − 노선이격 초과분)로 접근 구간에서 멈춤·점프 없이 측점값 연속 진행 - 노선밖 판정도 시선 기준: 화면이 노선을 비추기 시작하면 배지가 측점값으로 조기 전환, '노선밖 ○m' 도 시선 지점 기준 - 시점·시설물 마크와 배지 기준 통일(viewChain): 오프셋 활성 시 물리(드론 연직) 폴백 금지 — 구간 밖 시설물은 해당 세그먼트 바에 미표시(귀덕천교 사례), 세그먼트별 진단 로그(표본 Δ) 추가 - 스테이션바 1km(1+000) 단위 측점 마크: 측점값 라벨만 표시(이름 라벨·그룹핑 제외), 기존 마크와 겹침 시 생략 + 양끝 100m 내 시설물 없으면 시작/끝 스테이션 표시 - 팬(회전) 지연 이질감 보정: 프레임 동기 데이터의 POI 라벨 화면 EMA 우회(즉시 투영) + 자세 선행(attLeadSec) 파라미터 — 짐벌 로그 기록 지연 상쇄(위치는 유지, 자세만 앞선 시점 참조), 카메라 패널 슬라이더(-1~+1s) - 배지 시선 중심 자동 모드: AGL/tan|짐벌피치| 전방 주시거리(상한 350m), 진행방향 = 카메라 yaw·노선 접선 내적 - 기타: sen H 최대 100mm, F2 개발자 모드 토글(직전 커밋 누락분 아님 — 이번 범위) - 데이터 작업(코드 외): 경로 1-2/1-3/2 카메라·화면설정 산출 적용(yawOffset 실측, 경로표고 중앙값) 및 1-3 기존 시각보정 복원 — docs/history 참조 - docs/history: 작업 이력 15건 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,11 +34,16 @@ interface GeoPoint {
|
||||
}
|
||||
|
||||
/** 프레임별 precompute 결과. km=배지용 최근접측점, chain=구간방향용 연속 체이니지,
|
||||
* offM=측점 폴리라인까지 수평거리(m) — 노선 밖(접근/이탈) 판별용. */
|
||||
* offM=측점 폴리라인까지 수평거리(m) — 노선 밖(접근/이탈) 판별용.
|
||||
* viewChain=카메라 시선 중심이 닿는 지면 교점의 체이니지(커서 배지용) —
|
||||
* 드론 연직 체이니지(chain)보다 전방 주시거리(AGL/tan|pitch|)만큼 앞. */
|
||||
interface ViewedPoint {
|
||||
frameNum: number;
|
||||
km: number;
|
||||
chain: number;
|
||||
viewChain: number;
|
||||
/** 배지 표시 지점(시선/오프셋 지점)의 노선 이격(m) — 오프셋 미설정이면 offM 과 동일. */
|
||||
viewOffM: number;
|
||||
offM: number;
|
||||
time: number;
|
||||
}
|
||||
@@ -49,12 +54,11 @@ interface StationLine {
|
||||
k: number; // 경도→m 환산 (cos(lat0)*111000)
|
||||
}
|
||||
|
||||
/** 드론 lat/lon 을 측점 폴리라인에 투영 → 연속 체이니지(m) + 폴리라인까지 수평거리(m).
|
||||
* 100m 양자화된 최근접측점과 달리 전진/후진 전환 시점이 실제 이동과 일치.
|
||||
* distM 은 노선 밖(접근/이탈) 판별에 사용 — 밖에서는 chain 이 끝점에 클램프됨. */
|
||||
function projectChainageDist(lat: number, lon: number, line: StationLine): { chain: number; distM: number } {
|
||||
const px = lon * line.k;
|
||||
const py = lat * 111000;
|
||||
/** 노선 이탈 판정 거리(m) — 이 이상 벗어나면 접근/이탈 구간으로 취급. */
|
||||
const OFF_ROUTE_M = 60;
|
||||
|
||||
/** 평면 투영 좌표(px,py)를 측점 폴리라인에 투영 → 연속 체이니지(m) + 수평거리(m). */
|
||||
function projectXYChainage(px: number, py: number, line: StationLine): { chain: number; distM: number } {
|
||||
const pts = line.pts;
|
||||
let bestD = Infinity;
|
||||
let bestKm = pts.length ? pts[0].km : -1;
|
||||
@@ -76,6 +80,13 @@ function projectChainageDist(lat: number, lon: number, line: StationLine): { cha
|
||||
return { chain: bestKm, distM: isFinite(bestD) ? Math.sqrt(bestD) : Infinity };
|
||||
}
|
||||
|
||||
/** 드론 lat/lon 을 측점 폴리라인에 투영 → 연속 체이니지(m) + 폴리라인까지 수평거리(m).
|
||||
* 100m 양자화된 최근접측점과 달리 전진/후진 전환 시점이 실제 이동과 일치.
|
||||
* distM 은 노선 밖(접근/이탈) 판별에 사용 — 밖에서는 chain 이 끝점에 클램프됨. */
|
||||
function projectChainageDist(lat: number, lon: number, line: StationLine): { chain: number; distM: number } {
|
||||
return projectXYChainage(lon * line.k, lat * 111000, line);
|
||||
}
|
||||
|
||||
function projectChainage(lat: number, lon: number, line: StationLine): number {
|
||||
return projectChainageDist(lat, lon, line).chain;
|
||||
}
|
||||
@@ -283,17 +294,110 @@ export function StationBar({
|
||||
}
|
||||
const n = frames.length;
|
||||
const out: ViewedPoint[] = new Array(n);
|
||||
// ── 시선 중심 체이니지(viewChain) 준비 ─────────────────────────────
|
||||
// route.json routeInfo.station_lookahead(m) 설정:
|
||||
// 미설정/0 → 기존 그대로: 드론 연직(바로 아래) 체이니지 (기본)
|
||||
// N(숫자) → 드론 체이니지 + 진행방향으로 N(m) 고정 오프셋
|
||||
// "auto" → 자동: 주시거리 ≈ AGL / tan(|짐벌 피치|)
|
||||
// (AGL = 드론고도 − 지면고도(측점 z 체이니지 보간, 동일 datum))
|
||||
// 진행방향 = 카메라 yaw 벡터 · 노선 접선 내적 부호.
|
||||
// 오프셋 활성 시 노선 밖(접근/이탈)에서는 '시선이 닿는 지점'을 노선에 투영해
|
||||
// 그 지점이 노선 위(OFF_ROUTE_M 이내)면 배지가 측점값으로 전환된다.
|
||||
const lookCfgRaw = (routeMeta?.routeInfo as Record<string, unknown> | undefined)?.station_lookahead
|
||||
?? (routeMeta as unknown as Record<string, unknown> | null)?.station_lookahead;
|
||||
const lookFixed: number | null =
|
||||
typeof lookCfgRaw === 'number' && isFinite(lookCfgRaw) && lookCfgRaw > 0 ? lookCfgRaw
|
||||
: typeof lookCfgRaw === 'string' && lookCfgRaw.trim() !== '' && isFinite(Number(lookCfgRaw)) && Number(lookCfgRaw) > 0 ? Number(lookCfgRaw)
|
||||
: null;
|
||||
const lookAuto = typeof lookCfgRaw === 'string' && lookCfgRaw.trim().toLowerCase() === 'auto';
|
||||
const lookModeText = lookAuto ? '자동(카메라 시선 중심)' : lookFixed !== null ? `고정 +${lookFixed}m (진행방향)` : '미설정 (드론 연직 — 기존 동작)';
|
||||
const zProf = stations
|
||||
.map((st) => ({ km: stationKm(st.title), z: st.z }))
|
||||
.filter((p) => p.km >= 0 && p.z > 0)
|
||||
.sort((a, b) => a.km - b.km);
|
||||
const zAt = (km: number): number | null => {
|
||||
if (zProf.length < 2) return null;
|
||||
if (km <= zProf[0].km) return zProf[0].z;
|
||||
if (km >= zProf[zProf.length - 1].km) return zProf[zProf.length - 1].z;
|
||||
let lo = 0, hi = zProf.length - 1;
|
||||
while (hi - lo > 1) { const m = (lo + hi) >> 1; if (zProf[m].km <= km) lo = m; else hi = m; }
|
||||
const a = zProf[lo], b = zProf[hi];
|
||||
const r = b.km > a.km ? (km - a.km) / (b.km - a.km) : 0;
|
||||
return a.z + (b.z - a.z) * r;
|
||||
};
|
||||
const MAX_LOOK = 350; // 전방 주시거리 상한(m) — POI 표시범위(기본 400)와 유사 수준
|
||||
const tangentAt = (km: number): { x: number; y: number } | null => {
|
||||
const pts = stationLine.pts;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const a = pts[i], b = pts[i + 1];
|
||||
if ((km - a.km) * (km - b.km) <= 0 && a.km !== b.km) {
|
||||
const s = b.km > a.km ? 1 : -1; // +km(체이니지 증가) 방향 접선
|
||||
const dx = (b.x - a.x) * s, dy = (b.y - a.y) * s;
|
||||
const L = Math.hypot(dx, dy);
|
||||
return L > 0 ? { x: dx / L, y: dy / L } : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// 노선 체이니지 범위(클램프용) — 측점 폴리라인 km 범위(z 프로파일 유무와 무관).
|
||||
let chMin = Infinity, chMax = -Infinity;
|
||||
for (const p of stationLine.pts) { if (p.km < chMin) chMin = p.km; if (p.km > chMax) chMax = p.km; }
|
||||
for (let i = 0; i < n; i++) {
|
||||
const f = frames[i];
|
||||
const pj = projectChainageDist(f.lat, f.lon, stationLine);
|
||||
let viewChain = pj.chain;
|
||||
let viewOffM = pj.distM;
|
||||
if (lookFixed !== null || lookAuto) {
|
||||
// 주시거리(m): 고정값 또는 자동(짐벌 피치·AGL)
|
||||
let lookDist: number | null = lookFixed;
|
||||
if (lookAuto) {
|
||||
const gz = zAt(pj.chain);
|
||||
if (gz !== null && isFinite(f.altitude)) {
|
||||
const agl = f.altitude - gz;
|
||||
if (agl > 5 && agl < 500) {
|
||||
const pitchDeg = isFinite(f.pitch) ? f.pitch : -15;
|
||||
lookDist = pitchDeg <= -3
|
||||
? Math.min(MAX_LOOK, agl / Math.tan((-pitchDeg * Math.PI) / 180))
|
||||
: MAX_LOOK; // 수평에 가까우면 상한 사용
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lookDist !== null && lookDist > 0 && isFinite(f.yaw)) {
|
||||
// 연속 모델: 유효 주시거리 = lookDist − (노선 이격의 초과분).
|
||||
// 노선 위(이격 ≤ OFF_ROUTE_M): 초과분 0 → 항상 +lookDist (횡 이동에 안정)
|
||||
// 접근/이탈: 이격이 줄수록 유효 주시거리가 0→lookDist 로 늘어
|
||||
// 배지가 0+000 에서 멈춤·점프 없이 진입 후 +lookDist 까지 매끄럽게 이어진다.
|
||||
const over = Math.max(0, pj.distM - OFF_ROUTE_M);
|
||||
const effLook = lookDist - over;
|
||||
if (effLook >= 0) {
|
||||
const t = tangentAt(pj.chain);
|
||||
if (t) {
|
||||
const yawRad = (f.yaw * Math.PI) / 180;
|
||||
const dot = Math.sin(yawRad) * t.x + Math.cos(yawRad) * t.y;
|
||||
viewChain = clamp(pj.chain + (dot >= 0 ? 1 : -1) * effLook, chMin, chMax);
|
||||
}
|
||||
viewOffM = Math.min(pj.distM, OFF_ROUTE_M); // 값 표시 상태(노선밖 아님)
|
||||
} else {
|
||||
// 아직 시선이 노선에 못 미침 — '노선밖 (남은 거리+θ)m'
|
||||
viewOffM = OFF_ROUTE_M - effLook;
|
||||
}
|
||||
}
|
||||
}
|
||||
out[i] = {
|
||||
frameNum: f.frame,
|
||||
km: nearestStationKm(f, stations),
|
||||
chain: pj.chain,
|
||||
viewChain,
|
||||
viewOffM,
|
||||
offM: pj.distM,
|
||||
time: f.frame / videoFps,
|
||||
};
|
||||
}
|
||||
// 진단: 세그먼트별 적용 확인용 — 중간 프레임 표본의 연직 대비 오프셋(Δ).
|
||||
const midVp = out[Math.floor(out.length / 2)];
|
||||
console.log(
|
||||
`[스테이션바] 배지 오프셋: ${lookModeText} | 표본 Δ${Math.round(midVp.viewChain - midVp.chain) >= 0 ? '+' : ''}${Math.round(midVp.viewChain - midVp.chain)}m (${out.length}행)`,
|
||||
);
|
||||
// 이동거리 축(frac): 체이니지 |Δ| 가 아니라 GPS 실이동거리 누적(±W 이동평균 평활).
|
||||
// 체이니지 투영은 노선 밖(접근/이탈 구간)에서 폴리라인 끝점(0k000 등)에 클램프되어
|
||||
// 드론이 움직여도 커서가 멈추는 문제가 있었음(제주 — 비행 시작점이 노선 밖 700m).
|
||||
@@ -333,7 +437,7 @@ export function StationBar({
|
||||
chainRef.current = { time, frac, depChain: dep, arrChain: arr };
|
||||
viewedRef.current = out;
|
||||
setReady(true);
|
||||
}, [stations, stationLine, storeFrames, videoFps]);
|
||||
}, [stations, stationLine, storeFrames, videoFps, routeMeta]);
|
||||
|
||||
/** 현재 시간에 가장 가까운 precompute 인덱스. */
|
||||
const viewedIdxAtTime = useCallback((t: number): number => {
|
||||
@@ -352,18 +456,21 @@ export function StationBar({
|
||||
return best;
|
||||
}, [videoFps]);
|
||||
|
||||
// 현재 보는 측점값 — 연속 체이니지(chain)로 10m 해상도 (km 필드는 100m 양자화).
|
||||
// 현재 보는 측점값 — station_lookahead 미설정이면 드론 연직 체이니지(기존),
|
||||
// 설정 시 진행방향 오프셋/시선 중심 지점의 체이니지(viewChain).
|
||||
const realChain = useMemo<number | null>(() => {
|
||||
if (!ready) return null;
|
||||
const idx = viewedIdxAtTime(currentTime);
|
||||
return idx >= 0 ? viewedRef.current[idx].chain : null;
|
||||
return idx >= 0 ? viewedRef.current[idx].viewChain : null;
|
||||
}, [currentTime, ready, viewedIdxAtTime]);
|
||||
|
||||
// 현재 드론의 노선(측점 폴리라인) 이탈 거리(m) — 접근/이탈 구간 배지 표기용.
|
||||
// 배지 표시 지점의 노선 이격(m) — 접근/이탈 구간 '노선밖' 표기용.
|
||||
// 오프셋 활성 시 '시선이 닿는 지점' 기준이라, 화면이 노선을 비추기 시작하면
|
||||
// 드론이 아직 노선 밖이어도 배지가 측점값으로 먼저 전환된다.
|
||||
const realOffM = useMemo<number | null>(() => {
|
||||
if (!ready) return null;
|
||||
const idx = viewedIdxAtTime(currentTime);
|
||||
return idx >= 0 ? viewedRef.current[idx].offM : null;
|
||||
return idx >= 0 ? viewedRef.current[idx].viewOffM : null;
|
||||
}, [currentTime, ready, viewedIdxAtTime]);
|
||||
|
||||
// 종점역 '미도착' 폭(px) — 영상이 종점역 도착 전 끝날 때, 트랙 우측에 이만큼 비워둔다.
|
||||
@@ -460,7 +567,6 @@ export function StationBar({
|
||||
// 커서 배지 = 폴더 데이터 기반 연속 체이니지(realChain)를 10m 단위로 표시. 데이터 없으면 빈 문자열.
|
||||
// 드론이 노선(측점 폴리라인)에서 OFF_ROUTE_M 이상 벗어난 구간(접근/이탈)은 체이니지가
|
||||
// 끝점에 클램프되어 무의미 → "노선밖 ○○m" 로 표기해 측점이 안 변하는 이유를 드러낸다.
|
||||
const OFF_ROUTE_M = 60;
|
||||
const cursorText =
|
||||
realChain !== null && realChain >= 0
|
||||
? (realOffM !== null && realOffM > OFF_ROUTE_M
|
||||
@@ -561,6 +667,9 @@ export function StationBar({
|
||||
const arr = viewedRef.current;
|
||||
const frames = storeFrames;
|
||||
if (!ready || !arr.length || duration <= 0 || !frames.length) return [];
|
||||
// station_lookahead(시선/오프셋) 활성 여부 — viewChain 이 연직 체이니지와 다르면 활성.
|
||||
// 활성 시 시설물 마크는 viewChain 기준으로만 배치(물리 폴백 금지).
|
||||
const lookActive = arr.some((v) => Math.abs(v.viewChain - v.chain) > 1);
|
||||
|
||||
// 실좌표(lat/lon)에 드론이 가까워지는 '모든 통과 구간'의 px 목록.
|
||||
// (드론이 같은 구조물을 2번 이상 지날 때 각 통과마다 마커를 찍기 위함)
|
||||
@@ -619,6 +728,9 @@ export function StationBar({
|
||||
};
|
||||
|
||||
// 측점값(미터)에 해당하는 모든 통과 지점 px (연속 체이니지 기준).
|
||||
// 배지와 동일한 viewChain(오프셋/시선 기준) 사용 — station_lookahead 설정 시
|
||||
// 마크(시점·측점 기반 구조물)가 배지 값과 같은 타이밍에 커서와 만난다.
|
||||
// (미설정이면 viewChain=연직 체이니지 — 기존 동작 그대로)
|
||||
const pxPassesAtMileage = (targetM: number): { px: number; km: number }[] => {
|
||||
const arr = viewedRef.current;
|
||||
if (!arr.length) return [];
|
||||
@@ -628,13 +740,13 @@ export function StationBar({
|
||||
const out: { px: number; km: number }[] = [];
|
||||
let i = 0;
|
||||
while (i < arr.length) {
|
||||
// 노선 밖(접근/이탈) 프레임 제외 — 폴리라인 밖에선 체이니지가 끝점(0 등)에 클램프되어
|
||||
// 접근 구간 전체가 '시점(0k000) 통과'로 오인돼 마크가 바 맨 앞에 찍히는 문제 방지.
|
||||
if (arr[i].offM > OFF_ROUTE_M) { i++; continue; }
|
||||
if (Math.abs(arr[i].chain - targetM) < TH) {
|
||||
let j = i, bj = i, bd = Math.abs(arr[i].chain - targetM);
|
||||
while (j < arr.length && arr[j].offM <= OFF_ROUTE_M && Math.abs(arr[j].chain - targetM) < TH) {
|
||||
const d = Math.abs(arr[j].chain - targetM);
|
||||
// 배지 표시 지점이 노선 밖인 프레임 제외 — 접근/이탈 구간에서 체이니지가
|
||||
// 끝점(0 등)에 클램프되어 '시점 통과'로 오인돼 마크가 앞에 찍히는 문제 방지.
|
||||
if (arr[i].viewOffM > OFF_ROUTE_M) { i++; continue; }
|
||||
if (Math.abs(arr[i].viewChain - targetM) < TH) {
|
||||
let j = i, bj = i, bd = Math.abs(arr[i].viewChain - targetM);
|
||||
while (j < arr.length && arr[j].viewOffM <= OFF_ROUTE_M && Math.abs(arr[j].viewChain - targetM) < TH) {
|
||||
const d = Math.abs(arr[j].viewChain - targetM);
|
||||
if (d < bd) { bd = d; bj = j; }
|
||||
j++;
|
||||
}
|
||||
@@ -665,14 +777,14 @@ export function StationBar({
|
||||
const appendEndpointStations = (marks: StructMark[]): StructMark[] => {
|
||||
const va = viewedRef.current;
|
||||
if (!va.length) return marks;
|
||||
const onRoute = va.filter(v => v.offM <= OFF_ROUTE_M);
|
||||
const onRoute = va.filter(v => v.viewOffM <= 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;
|
||||
const ch = e.v.viewChain;
|
||||
if (marks.some(m => Math.abs(m.km - ch) <= 100)) continue;
|
||||
const px = pxAtTime(e.v.time);
|
||||
if (px === null) continue;
|
||||
@@ -681,6 +793,22 @@ export function StationBar({
|
||||
return marks;
|
||||
};
|
||||
|
||||
// 1km(1+000) 단위 측점 마크 — 매 1000m 측점의 통과 위치에 측점값 라벨(위)만 표시.
|
||||
// title 은 빈 문자열 → 하단 이름 라벨/그룹핑 제외(Timeline 에서 스킵).
|
||||
// 기존 마크(시점·시설물 등)와 24px 이내로 겹치면 생략(라벨 충돌 방지).
|
||||
const appendKmStations = (marks: StructMark[]): StructMark[] => {
|
||||
if (!stationLine) return marks;
|
||||
let lo = Infinity, hi = -Infinity;
|
||||
for (const p of stationLine.pts) { if (p.km < lo) lo = p.km; if (p.km > hi) hi = p.km; }
|
||||
for (let m = Math.ceil(lo / 1000) * 1000; m <= hi; m += 1000) {
|
||||
for (const p of pxPassesAtMileage(m)) {
|
||||
if (marks.some((x) => Math.abs(x.px - p.px) < 24)) continue;
|
||||
marks.push({ px: p.px, title: '', category: '측점', km: m, kmExists: true });
|
||||
}
|
||||
}
|
||||
return marks;
|
||||
};
|
||||
|
||||
// 구조물 후보 POI(교량/터널/역사) base 이름 → 실좌표.
|
||||
const cats = new Set(['교량', '터널', '역사']);
|
||||
const poiByName = new Map<string, { lat: number; lon: number; category: string }>();
|
||||
@@ -744,12 +872,22 @@ export function StationBar({
|
||||
// 일치(pxPassesAtMileage)'하는 통과에서만 마커를 찍는다. 그래야:
|
||||
// ① 좌표만 가까운(측점 다른) 조차장 재진입(드론 162k210인데 대전조차장 162k080)은 안 찍히고,
|
||||
// ② 진짜 162k080 통과에는 정상적으로 찍힌다. (교량/터널만 좌표근접 폴백 허용.)
|
||||
if (!passes.length && cat !== '역사' && s.lat != null && s.lon != null) passes = pxPassesTo(s.lat, s.lon, off);
|
||||
if (!passes.length && cat !== '역사' && match) passes = pxPassesTo(match.lat, match.lon, off);
|
||||
// ※ station_lookahead(시선/오프셋) 활성 시 물리(드론 연직) 폴백 금지 —
|
||||
// 구간 밖 시설물(예: 이전 세그먼트에서 이미 바라본 교량)이 물리 통과 위치로
|
||||
// 끌려 들어와 배지 기준(viewChain 범위)에 없는 값이 바에 찍히는 것을 방지.
|
||||
// 해당 시설물은 자기 viewChain 범위를 가진 세그먼트의 바에서 정상 표시된다.
|
||||
if (!lookActive) {
|
||||
if (!passes.length && cat !== '역사' && s.lat != null && s.lon != null) passes = pxPassesTo(s.lat, s.lon, off);
|
||||
if (!passes.length && cat !== '역사' && match) passes = pxPassesTo(match.lat, match.lon, off);
|
||||
}
|
||||
if (!passes.length && s.startMileage != null && s.endMileage != null) {
|
||||
const mid = (s.startMileage + s.endMileage) / 2;
|
||||
const px = pxAtMileage(mid);
|
||||
if (px !== null) passes = [{ px, km: mid }];
|
||||
if (lookActive) {
|
||||
passes = pxPassesAtMileage(mid); // 시선 기준 통과 — 범위 밖이면 미표시
|
||||
} else {
|
||||
const px = pxAtMileage(mid);
|
||||
if (px !== null) passes = [{ px, km: mid }];
|
||||
}
|
||||
}
|
||||
// 역사 km 라벨 = '역 좌표 자체'의 측점값(드론 최근접 프레임 chain 아님).
|
||||
// 역은 바 양끝에 스냅되므로, 시작/끝 커서값(= 역 위치 측점)과 일치시키기 위함.
|
||||
@@ -763,7 +901,7 @@ export function StationBar({
|
||||
out.push({ px: p.px, title: s.name, category: cat, km: stationKmVal ?? p.km, kmExists, props: s.props });
|
||||
}
|
||||
}
|
||||
return appendEndpointStations(placeUnreachedTerminal(appendStartMark(out)));
|
||||
return appendKmStations(appendEndpointStations(placeUnreachedTerminal(appendStartMark(out))));
|
||||
}
|
||||
|
||||
// 폴백: POI category 기반 (실좌표 GPS 근접, 모든 통과).
|
||||
@@ -782,7 +920,7 @@ export function StationBar({
|
||||
out.push({ px: p.px, title: base, category: info.category, km: stationKmVal ?? p.km, kmExists });
|
||||
}
|
||||
}
|
||||
return appendEndpointStations(placeUnreachedTerminal(appendStartMark(out)));
|
||||
return appendKmStations(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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user